1# Contributors Listed Below - COPYRIGHT 2016 2# [+] International Business Machines Corp. 3# 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14# implied. See the License for the specific language governing 15# permissions and limitations under the License. 16 17import os 18import dbus 19import dbus.exceptions 20import json 21from xml.etree import ElementTree 22from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError 23from bottle import static_file 24import obmc.utils.misc 25from obmc.dbuslib.introspection import IntrospectionNodeParser 26import obmc.mapper 27import spwd 28import grp 29import crypt 30import tempfile 31import re 32have_wsock = True 33try: 34 from geventwebsocket import WebSocketError 35except ImportError: 36 have_wsock = False 37if have_wsock: 38 from dbus.mainloop.glib import DBusGMainLoop 39 DBusGMainLoop(set_as_default=True) 40 import gobject 41 import gevent 42 43DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface' 44DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface' 45DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' 46DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs' 47DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError' 48DELETE_IFACE = 'xyz.openbmc_project.Object.Delete' 49 50_4034_msg = "The specified %s cannot be %s: '%s'" 51 52 53def valid_user(session, *a, **kw): 54 ''' Authorization plugin callback that checks 55 that the user is logged in. ''' 56 if session is None: 57 abort(401, 'Login required') 58 59 60def get_type_signature_by_introspection(bus, service, object_path, 61 property_name): 62 obj = bus.get_object(service, object_path) 63 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable') 64 xml_string = iface.Introspect() 65 for child in ElementTree.fromstring(xml_string): 66 # Iterate over each interfaces's properties to find 67 # matching property_name, and return its signature string 68 if child.tag == 'interface': 69 for i in child.iter(): 70 if ('name' in i.attrib) and \ 71 (i.attrib['name'] == property_name): 72 type_signature = i.attrib['type'] 73 return type_signature 74 75 76def get_method_signature(bus, service, object_path, interface, method): 77 obj = bus.get_object(service, object_path) 78 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable') 79 xml_string = iface.Introspect() 80 arglist = [] 81 82 root = ElementTree.fromstring(xml_string) 83 for dbus_intf in root.findall('interface'): 84 if (dbus_intf.get('name') == interface): 85 for dbus_method in dbus_intf.findall('method'): 86 if(dbus_method.get('name') == method): 87 for arg in dbus_method.findall('arg'): 88 arglist.append(arg.get('type')) 89 return arglist 90 91 92def split_struct_signature(signature): 93 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?' 94 struct_matches = re.findall(struct_regex, signature) 95 return struct_matches 96 97 98def convert_type(signature, value): 99 # Basic Types 100 converted_value = None 101 converted_container = None 102 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int, 103 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32, 104 't': dbus.UInt64, 'd': float, 's': str} 105 array_matches = re.match(r'a\((\S+)\)', signature) 106 struct_matches = re.match(r'\((\S+)\)', signature) 107 dictionary_matches = re.match(r'a{(\S+)}', signature) 108 if signature in basic_types: 109 converted_value = basic_types[signature](value) 110 return converted_value 111 # Array 112 if array_matches: 113 element_type = array_matches.group(1) 114 converted_container = list() 115 # Test if value is a list 116 # to avoid iterating over each character in a string. 117 # Iterate over each item and convert type 118 if isinstance(value, list): 119 for i in value: 120 converted_element = convert_type(element_type, i) 121 converted_container.append(converted_element) 122 # Convert non-sequence to expected type, and append to list 123 else: 124 converted_element = convert_type(element_type, value) 125 converted_container.append(converted_element) 126 return converted_container 127 # Struct 128 if struct_matches: 129 element_types = struct_matches.group(1) 130 split_element_types = split_struct_signature(element_types) 131 converted_container = list() 132 # Test if value is a list 133 if isinstance(value, list): 134 for index, val in enumerate(value): 135 converted_element = convert_type(split_element_types[index], 136 value[index]) 137 converted_container.append(converted_element) 138 else: 139 converted_element = convert_type(element_types, value) 140 converted_container.append(converted_element) 141 return tuple(converted_container) 142 # Dictionary 143 if dictionary_matches: 144 element_types = dictionary_matches.group(1) 145 split_element_types = split_struct_signature(element_types) 146 converted_container = dict() 147 # Convert each element of dict 148 for key, val in value.iteritems(): 149 converted_key = convert_type(split_element_types[0], key) 150 converted_val = convert_type(split_element_types[1], val) 151 converted_container[converted_key] = converted_val 152 return converted_container 153 154 155class UserInGroup: 156 ''' Authorization plugin callback that checks that the user is logged in 157 and a member of a group. ''' 158 def __init__(self, group): 159 self.group = group 160 161 def __call__(self, session, *a, **kw): 162 valid_user(session, *a, **kw) 163 res = False 164 165 try: 166 res = session['user'] in grp.getgrnam(self.group)[3] 167 except KeyError: 168 pass 169 170 if not res: 171 abort(403, 'Insufficient access') 172 173 174class RouteHandler(object): 175 _require_auth = obmc.utils.misc.makelist(valid_user) 176 _enable_cors = True 177 178 def __init__(self, app, bus, verbs, rules, content_type=''): 179 self.app = app 180 self.bus = bus 181 self.mapper = obmc.mapper.Mapper(bus) 182 self._verbs = obmc.utils.misc.makelist(verbs) 183 self._rules = rules 184 self._content_type = content_type 185 186 if 'GET' in self._verbs: 187 self._verbs = list(set(self._verbs + ['HEAD'])) 188 if 'OPTIONS' not in self._verbs: 189 self._verbs.append('OPTIONS') 190 191 def _setup(self, **kw): 192 request.route_data = {} 193 194 if request.method in self._verbs: 195 if request.method != 'OPTIONS': 196 return self.setup(**kw) 197 198 # Javascript implementations will not send credentials 199 # with an OPTIONS request. Don't help malicious clients 200 # by checking the path here and returning a 404 if the 201 # path doesn't exist. 202 return None 203 204 # Return 405 205 raise HTTPError( 206 405, "Method not allowed.", Allow=','.join(self._verbs)) 207 208 def __call__(self, **kw): 209 return getattr(self, 'do_' + request.method.lower())(**kw) 210 211 def do_head(self, **kw): 212 return self.do_get(**kw) 213 214 def do_options(self, **kw): 215 for v in self._verbs: 216 response.set_header( 217 'Allow', 218 ','.join(self._verbs)) 219 return None 220 221 def install(self): 222 self.app.route( 223 self._rules, callback=self, 224 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE']) 225 226 @staticmethod 227 def try_mapper_call(f, callback=None, **kw): 228 try: 229 return f(**kw) 230 except dbus.exceptions.DBusException, e: 231 if e.get_dbus_name() == \ 232 'org.freedesktop.DBus.Error.ObjectPathInUse': 233 abort(503, str(e)) 234 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND: 235 raise 236 if callback is None: 237 def callback(e, **kw): 238 abort(404, str(e)) 239 240 callback(e, **kw) 241 242 @staticmethod 243 def try_properties_interface(f, *a): 244 try: 245 return f(*a) 246 except dbus.exceptions.DBusException, e: 247 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message(): 248 # interface doesn't have any properties 249 return None 250 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name(): 251 # interface doesn't have any properties 252 return None 253 if DBUS_UNKNOWN_METHOD == e.get_dbus_name(): 254 # properties interface not implemented at all 255 return None 256 raise 257 258 259class DirectoryHandler(RouteHandler): 260 verbs = 'GET' 261 rules = '<path:path>/' 262 263 def __init__(self, app, bus): 264 super(DirectoryHandler, self).__init__( 265 app, bus, self.verbs, self.rules) 266 267 def find(self, path='/'): 268 return self.try_mapper_call( 269 self.mapper.get_subtree_paths, path=path, depth=1) 270 271 def setup(self, path='/'): 272 request.route_data['map'] = self.find(path) 273 274 def do_get(self, path='/'): 275 return request.route_data['map'] 276 277 278class ListNamesHandler(RouteHandler): 279 verbs = 'GET' 280 rules = ['/list', '<path:path>/list'] 281 282 def __init__(self, app, bus): 283 super(ListNamesHandler, self).__init__( 284 app, bus, self.verbs, self.rules) 285 286 def find(self, path='/'): 287 return self.try_mapper_call( 288 self.mapper.get_subtree, path=path).keys() 289 290 def setup(self, path='/'): 291 request.route_data['map'] = self.find(path) 292 293 def do_get(self, path='/'): 294 return request.route_data['map'] 295 296 297class ListHandler(RouteHandler): 298 verbs = 'GET' 299 rules = ['/enumerate', '<path:path>/enumerate'] 300 301 def __init__(self, app, bus): 302 super(ListHandler, self).__init__( 303 app, bus, self.verbs, self.rules) 304 305 def find(self, path='/'): 306 return self.try_mapper_call( 307 self.mapper.get_subtree, path=path) 308 309 def setup(self, path='/'): 310 request.route_data['map'] = self.find(path) 311 312 def do_get(self, path='/'): 313 return {x: y for x, y in self.mapper.enumerate_subtree( 314 path, 315 mapper_data=request.route_data['map']).dataitems()} 316 317 318class MethodHandler(RouteHandler): 319 verbs = 'POST' 320 rules = '<path:path>/action/<method>' 321 request_type = list 322 content_type = 'application/json' 323 324 def __init__(self, app, bus): 325 super(MethodHandler, self).__init__( 326 app, bus, self.verbs, self.rules, self.content_type) 327 self.service = '' 328 self.interface = '' 329 330 def find(self, path, method): 331 method_list = [] 332 busses = self.try_mapper_call( 333 self.mapper.get_object, path=path) 334 for items in busses.iteritems(): 335 m = self.find_method_on_bus(path, method, *items) 336 if m: 337 method_list.append(m) 338 if method_list: 339 return method_list 340 341 abort(404, _4034_msg % ('method', 'found', method)) 342 343 def setup(self, path, method): 344 request.route_data['map'] = self.find(path, method) 345 346 def do_post(self, path, method): 347 try: 348 args = [] 349 if request.parameter_list: 350 args = request.parameter_list 351 # To see if the return type is capable of being merged 352 if len(request.route_data['map']) > 1: 353 results = None 354 for item in request.route_data['map']: 355 tmp = item(*args) 356 if not results: 357 if tmp is not None: 358 results = type(tmp)() 359 if isinstance(results, dict): 360 results = results.update(tmp) 361 elif isinstance(results, list): 362 results = results + tmp 363 elif isinstance(results, type(None)): 364 results = None 365 else: 366 abort(501, 'Don\'t know how to merge method call ' 367 'results of {}'.format(type(tmp))) 368 return results 369 # There is only one method 370 return request.route_data['map'][0](*args) 371 372 except dbus.exceptions.DBusException, e: 373 paramlist = [] 374 if e.get_dbus_name() == DBUS_INVALID_ARGS: 375 376 signature_list = get_method_signature(self.bus, self.service, 377 path, self.interface, 378 method) 379 if not signature_list: 380 abort(400, "Failed to get method signature: %s" % str(e)) 381 if len(signature_list) != len(request.parameter_list): 382 abort(400, "Invalid number of args") 383 converted_value = None 384 try: 385 for index, expected_type in enumerate(signature_list): 386 value = request.parameter_list[index] 387 converted_value = convert_type(expected_type, value) 388 paramlist.append(converted_value) 389 request.parameter_list = paramlist 390 self.do_post(path, method) 391 return 392 except Exception as ex: 393 abort(400, "Failed to convert the types") 394 abort(400, str(e)) 395 396 if e.get_dbus_name() == DBUS_TYPE_ERROR: 397 abort(400, str(e)) 398 raise 399 400 @staticmethod 401 def find_method_in_interface(method, obj, interface, methods): 402 if methods is None: 403 return None 404 405 method = obmc.utils.misc.find_case_insensitive(method, methods.keys()) 406 if method is not None: 407 iface = dbus.Interface(obj, interface) 408 return iface.get_dbus_method(method) 409 410 def find_method_on_bus(self, path, method, bus, interfaces): 411 obj = self.bus.get_object(bus, path, introspect=False) 412 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 413 data = iface.Introspect() 414 parser = IntrospectionNodeParser( 415 ElementTree.fromstring(data), 416 intf_match=obmc.utils.misc.ListMatch(interfaces)) 417 for x, y in parser.get_interfaces().iteritems(): 418 m = self.find_method_in_interface( 419 method, obj, x, y.get('method')) 420 if m: 421 self.service = bus 422 self.interface = x 423 return m 424 425 426class PropertyHandler(RouteHandler): 427 verbs = ['PUT', 'GET'] 428 rules = '<path:path>/attr/<prop>' 429 content_type = 'application/json' 430 431 def __init__(self, app, bus): 432 super(PropertyHandler, self).__init__( 433 app, bus, self.verbs, self.rules, self.content_type) 434 435 def find(self, path, prop): 436 self.app.instance_handler.setup(path) 437 obj = self.app.instance_handler.do_get(path) 438 real_name = obmc.utils.misc.find_case_insensitive( 439 prop, obj.keys()) 440 441 if not real_name: 442 if request.method == 'PUT': 443 abort(403, _4034_msg % ('property', 'created', prop)) 444 else: 445 abort(404, _4034_msg % ('property', 'found', prop)) 446 return real_name, {path: obj} 447 448 def setup(self, path, prop): 449 name, obj = self.find(path, prop) 450 request.route_data['obj'] = obj 451 request.route_data['name'] = name 452 453 def do_get(self, path, prop): 454 name = request.route_data['name'] 455 return request.route_data['obj'][path][name] 456 457 def do_put(self, path, prop, value=None): 458 if value is None: 459 value = request.parameter_list 460 461 prop, iface, properties_iface = self.get_host_interface( 462 path, prop, request.route_data['map'][path]) 463 try: 464 properties_iface.Set(iface, prop, value) 465 except ValueError, e: 466 abort(400, str(e)) 467 except dbus.exceptions.DBusException, e: 468 if e.get_dbus_name() == DBUS_INVALID_ARGS: 469 bus_name = properties_iface.bus_name 470 expected_type = get_type_signature_by_introspection(self.bus, 471 bus_name, 472 path, 473 prop) 474 if not expected_type: 475 abort(403, "Failed to get expected type: %s" % str(e)) 476 converted_value = None 477 try: 478 converted_value = convert_type(expected_type, value) 479 self.do_put(path, prop, converted_value) 480 return 481 except Exception as ex: 482 abort(403, "Failed to convert %s to type %s" % 483 (value, expected_type)) 484 abort(403, str(e)) 485 raise 486 487 def get_host_interface(self, path, prop, bus_info): 488 for bus, interfaces in bus_info.iteritems(): 489 obj = self.bus.get_object(bus, path, introspect=True) 490 properties_iface = dbus.Interface( 491 obj, dbus_interface=dbus.PROPERTIES_IFACE) 492 493 info = self.get_host_interface_on_bus( 494 path, prop, properties_iface, bus, interfaces) 495 if info is not None: 496 prop, iface = info 497 return prop, iface, properties_iface 498 499 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces): 500 for i in interfaces: 501 properties = self.try_properties_interface(iface.GetAll, i) 502 if not properties: 503 continue 504 match = obmc.utils.misc.find_case_insensitive( 505 prop, properties.keys()) 506 if match is None: 507 continue 508 prop = match 509 return prop, i 510 511 512class SchemaHandler(RouteHandler): 513 verbs = ['GET'] 514 rules = '<path:path>/schema' 515 516 def __init__(self, app, bus): 517 super(SchemaHandler, self).__init__( 518 app, bus, self.verbs, self.rules) 519 520 def find(self, path): 521 return self.try_mapper_call( 522 self.mapper.get_object, 523 path=path) 524 525 def setup(self, path): 526 request.route_data['map'] = self.find(path) 527 528 def do_get(self, path): 529 schema = {} 530 for x in request.route_data['map'].iterkeys(): 531 obj = self.bus.get_object(x, path, introspect=False) 532 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 533 data = iface.Introspect() 534 parser = IntrospectionNodeParser( 535 ElementTree.fromstring(data)) 536 for x, y in parser.get_interfaces().iteritems(): 537 schema[x] = y 538 539 return schema 540 541 542class InstanceHandler(RouteHandler): 543 verbs = ['GET', 'PUT', 'DELETE'] 544 rules = '<path:path>' 545 request_type = dict 546 547 def __init__(self, app, bus): 548 super(InstanceHandler, self).__init__( 549 app, bus, self.verbs, self.rules) 550 551 def find(self, path, callback=None): 552 return {path: self.try_mapper_call( 553 self.mapper.get_object, 554 callback, 555 path=path)} 556 557 def setup(self, path): 558 callback = None 559 if request.method == 'PUT': 560 def callback(e, **kw): 561 abort(403, _4034_msg % ('resource', 'created', path)) 562 563 if request.route_data.get('map') is None: 564 request.route_data['map'] = self.find(path, callback) 565 566 def do_get(self, path): 567 return self.mapper.enumerate_object( 568 path, 569 mapper_data=request.route_data['map']) 570 571 def do_put(self, path): 572 # make sure all properties exist in the request 573 obj = set(self.do_get(path).keys()) 574 req = set(request.parameter_list.keys()) 575 576 diff = list(obj.difference(req)) 577 if diff: 578 abort(403, _4034_msg % ( 579 'resource', 'removed', '%s/attr/%s' % (path, diff[0]))) 580 581 diff = list(req.difference(obj)) 582 if diff: 583 abort(403, _4034_msg % ( 584 'resource', 'created', '%s/attr/%s' % (path, diff[0]))) 585 586 for p, v in request.parameter_list.iteritems(): 587 self.app.property_handler.do_put( 588 path, p, v) 589 590 def do_delete(self, path): 591 for bus_info in request.route_data['map'][path].iteritems(): 592 if self.bus_missing_delete(path, *bus_info): 593 abort(403, _4034_msg % ('resource', 'removed', path)) 594 595 for bus in request.route_data['map'][path].iterkeys(): 596 self.delete_on_bus(path, bus) 597 598 def bus_missing_delete(self, path, bus, interfaces): 599 return DELETE_IFACE not in interfaces 600 601 def delete_on_bus(self, path, bus): 602 obj = self.bus.get_object(bus, path, introspect=False) 603 delete_iface = dbus.Interface( 604 obj, dbus_interface=DELETE_IFACE) 605 delete_iface.Delete() 606 607 608class SessionHandler(MethodHandler): 609 ''' Handles the /login and /logout routes, manages 610 server side session store and session cookies. ''' 611 612 rules = ['/login', '/logout'] 613 login_str = "User '%s' logged %s" 614 bad_passwd_str = "Invalid username or password" 615 no_user_str = "No user logged in" 616 bad_json_str = "Expecting request format { 'data': " \ 617 "[<username>, <password>] }, got '%s'" 618 _require_auth = None 619 MAX_SESSIONS = 16 620 621 def __init__(self, app, bus): 622 super(SessionHandler, self).__init__( 623 app, bus) 624 self.hmac_key = os.urandom(128) 625 self.session_store = [] 626 627 @staticmethod 628 def authenticate(username, clear): 629 try: 630 encoded = spwd.getspnam(username)[1] 631 return encoded == crypt.crypt(clear, encoded) 632 except KeyError: 633 return False 634 635 def invalidate_session(self, session): 636 try: 637 self.session_store.remove(session) 638 except ValueError: 639 pass 640 641 def new_session(self): 642 sid = os.urandom(32) 643 if self.MAX_SESSIONS <= len(self.session_store): 644 self.session_store.pop() 645 self.session_store.insert(0, {'sid': sid}) 646 647 return self.session_store[0] 648 649 def get_session(self, sid): 650 sids = [x['sid'] for x in self.session_store] 651 try: 652 return self.session_store[sids.index(sid)] 653 except ValueError: 654 return None 655 656 def get_session_from_cookie(self): 657 return self.get_session( 658 request.get_cookie( 659 'sid', secret=self.hmac_key)) 660 661 def do_post(self, **kw): 662 if request.path == '/login': 663 return self.do_login(**kw) 664 else: 665 return self.do_logout(**kw) 666 667 def do_logout(self, **kw): 668 session = self.get_session_from_cookie() 669 if session is not None: 670 user = session['user'] 671 self.invalidate_session(session) 672 response.delete_cookie('sid') 673 return self.login_str % (user, 'out') 674 675 return self.no_user_str 676 677 def do_login(self, **kw): 678 session = self.get_session_from_cookie() 679 if session is not None: 680 return self.login_str % (session['user'], 'in') 681 682 if len(request.parameter_list) != 2: 683 abort(400, self.bad_json_str % (request.json)) 684 685 if not self.authenticate(*request.parameter_list): 686 abort(401, self.bad_passwd_str) 687 688 user = request.parameter_list[0] 689 session = self.new_session() 690 session['user'] = user 691 response.set_cookie( 692 'sid', session['sid'], secret=self.hmac_key, 693 secure=True, 694 httponly=True) 695 return self.login_str % (user, 'in') 696 697 def find(self, **kw): 698 pass 699 700 def setup(self, **kw): 701 pass 702 703 704class ImageUploadUtils: 705 ''' Provides common utils for image upload. ''' 706 707 file_loc = '/tmp/images' 708 file_prefix = 'img' 709 file_suffix = '' 710 711 @classmethod 712 def do_upload(cls, filename=''): 713 if not os.path.exists(cls.file_loc): 714 abort(500, "Error Directory not found") 715 if not filename: 716 handle, filename = tempfile.mkstemp(cls.file_suffix, 717 cls.file_prefix, cls.file_loc) 718 else: 719 filename = os.path.join(cls.file_loc, filename) 720 handle = os.open(filename, os.O_WRONLY | os.O_CREAT) 721 try: 722 file_contents = request.body.read() 723 request.body.close() 724 os.write(handle, file_contents) 725 except (IOError, ValueError), e: 726 abort(400, str(e)) 727 except: 728 abort(400, "Unexpected Error") 729 finally: 730 os.close(handle) 731 732 733class ImagePostHandler(RouteHandler): 734 ''' Handles the /upload/image route. ''' 735 736 verbs = ['POST'] 737 rules = ['/upload/image'] 738 content_type = 'application/octet-stream' 739 740 def __init__(self, app, bus): 741 super(ImagePostHandler, self).__init__( 742 app, bus, self.verbs, self.rules, self.content_type) 743 744 def do_post(self, filename=''): 745 ImageUploadUtils.do_upload() 746 747 def find(self, **kw): 748 pass 749 750 def setup(self, **kw): 751 pass 752 753 754class EventNotifier: 755 keyNames = {} 756 keyNames['event'] = 'event' 757 keyNames['path'] = 'path' 758 keyNames['intfMap'] = 'interfaces' 759 keyNames['propMap'] = 'properties' 760 keyNames['intf'] = 'interface' 761 762 def __init__(self, wsock, filters): 763 self.wsock = wsock 764 self.paths = filters.get("paths", []) 765 self.interfaces = filters.get("interfaces", []) 766 if not self.paths: 767 self.paths.append(None) 768 bus = dbus.SystemBus() 769 # Add a signal receiver for every path the client is interested in 770 for path in self.paths: 771 bus.add_signal_receiver( 772 self.interfaces_added_handler, 773 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 774 signal_name='InterfacesAdded', 775 path=path) 776 bus.add_signal_receiver( 777 self.properties_changed_handler, 778 dbus_interface=dbus.PROPERTIES_IFACE, 779 signal_name='PropertiesChanged', 780 path=path, 781 path_keyword='path') 782 loop = gobject.MainLoop() 783 # gobject's mainloop.run() will block the entire process, so the gevent 784 # scheduler and hence greenlets won't execute. The while-loop below 785 # works around this limitation by using gevent's sleep, instead of 786 # calling loop.run() 787 gcontext = loop.get_context() 788 while loop is not None: 789 try: 790 if gcontext.pending(): 791 gcontext.iteration() 792 else: 793 # gevent.sleep puts only the current greenlet to sleep, 794 # not the entire process. 795 gevent.sleep(5) 796 except WebSocketError: 797 break 798 799 def interfaces_added_handler(self, path, iprops, **kw): 800 ''' If the client is interested in these changes, respond to the 801 client. This handles d-bus interface additions.''' 802 if (not self.interfaces) or \ 803 (not set(iprops).isdisjoint(self.interfaces)): 804 response = {} 805 response[self.keyNames['event']] = "InterfacesAdded" 806 response[self.keyNames['path']] = path 807 response[self.keyNames['intfMap']] = iprops 808 try: 809 self.wsock.send(json.dumps(response)) 810 except WebSocketError: 811 return 812 813 def properties_changed_handler(self, interface, new, old, **kw): 814 ''' If the client is interested in these changes, respond to the 815 client. This handles d-bus property changes. ''' 816 if (not self.interfaces) or (interface in self.interfaces): 817 path = str(kw['path']) 818 response = {} 819 response[self.keyNames['event']] = "PropertiesChanged" 820 response[self.keyNames['path']] = path 821 response[self.keyNames['intf']] = interface 822 response[self.keyNames['propMap']] = new 823 try: 824 self.wsock.send(json.dumps(response)) 825 except WebSocketError: 826 return 827 828 829class EventHandler(RouteHandler): 830 ''' Handles the /subscribe route, for clients to be able 831 to subscribe to BMC events. ''' 832 833 verbs = ['GET'] 834 rules = ['/subscribe'] 835 836 def __init__(self, app, bus): 837 super(EventHandler, self).__init__( 838 app, bus, self.verbs, self.rules) 839 840 def find(self, **kw): 841 pass 842 843 def setup(self, **kw): 844 pass 845 846 def do_get(self): 847 wsock = request.environ.get('wsgi.websocket') 848 if not wsock: 849 abort(400, 'Expected WebSocket request.') 850 filters = wsock.receive() 851 filters = json.loads(filters) 852 notifier = EventNotifier(wsock, filters) 853 854 855class ImagePutHandler(RouteHandler): 856 ''' Handles the /upload/image/<filename> route. ''' 857 858 verbs = ['PUT'] 859 rules = ['/upload/image/<filename>'] 860 content_type = 'application/octet-stream' 861 862 def __init__(self, app, bus): 863 super(ImagePutHandler, self).__init__( 864 app, bus, self.verbs, self.rules, self.content_type) 865 866 def do_put(self, filename=''): 867 ImageUploadUtils.do_upload(filename) 868 869 def find(self, **kw): 870 pass 871 872 def setup(self, **kw): 873 pass 874 875 876class DownloadDumpHandler(RouteHandler): 877 ''' Handles the /download/dump route. ''' 878 879 verbs = 'GET' 880 rules = ['/download/dump/<dumpid>'] 881 content_type = 'application/octet-stream' 882 dump_loc = '/var/lib/phosphor-debug-collector/dumps' 883 suppress_json_resp = True 884 885 def __init__(self, app, bus): 886 super(DownloadDumpHandler, self).__init__( 887 app, bus, self.verbs, self.rules, self.content_type) 888 889 def do_get(self, dumpid): 890 return self.do_download(dumpid) 891 892 def find(self, **kw): 893 pass 894 895 def setup(self, **kw): 896 pass 897 898 def do_download(self, dumpid): 899 dump_loc = os.path.join(self.dump_loc, dumpid) 900 if not os.path.exists(dump_loc): 901 abort(404, "Path not found") 902 903 files = os.listdir(dump_loc) 904 num_files = len(files) 905 if num_files == 0: 906 abort(404, "Dump not found") 907 908 return static_file(os.path.basename(files[0]), root=dump_loc, 909 download=True, mimetype=self.content_type) 910 911 912class AuthorizationPlugin(object): 913 ''' Invokes an optional list of authorization callbacks. ''' 914 915 name = 'authorization' 916 api = 2 917 918 class Compose: 919 def __init__(self, validators, callback, session_mgr): 920 self.validators = validators 921 self.callback = callback 922 self.session_mgr = session_mgr 923 924 def __call__(self, *a, **kw): 925 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) 926 session = self.session_mgr.get_session(sid) 927 if request.method != 'OPTIONS': 928 for x in self.validators: 929 x(session, *a, **kw) 930 931 return self.callback(*a, **kw) 932 933 def apply(self, callback, route): 934 undecorated = route.get_undecorated_callback() 935 if not isinstance(undecorated, RouteHandler): 936 return callback 937 938 auth_types = getattr( 939 undecorated, '_require_auth', None) 940 if not auth_types: 941 return callback 942 943 return self.Compose( 944 auth_types, callback, undecorated.app.session_handler) 945 946 947class CorsPlugin(object): 948 ''' Add CORS headers. ''' 949 950 name = 'cors' 951 api = 2 952 953 @staticmethod 954 def process_origin(): 955 origin = request.headers.get('Origin') 956 if origin: 957 response.add_header('Access-Control-Allow-Origin', origin) 958 response.add_header( 959 'Access-Control-Allow-Credentials', 'true') 960 961 @staticmethod 962 def process_method_and_headers(verbs): 963 method = request.headers.get('Access-Control-Request-Method') 964 headers = request.headers.get('Access-Control-Request-Headers') 965 if headers: 966 headers = [x.lower() for x in headers.split(',')] 967 968 if method in verbs \ 969 and headers == ['content-type']: 970 response.add_header('Access-Control-Allow-Methods', method) 971 response.add_header( 972 'Access-Control-Allow-Headers', 'Content-Type') 973 974 def __init__(self, app): 975 app.install_error_callback(self.error_callback) 976 977 def apply(self, callback, route): 978 undecorated = route.get_undecorated_callback() 979 if not isinstance(undecorated, RouteHandler): 980 return callback 981 982 if not getattr(undecorated, '_enable_cors', None): 983 return callback 984 985 def wrap(*a, **kw): 986 self.process_origin() 987 self.process_method_and_headers(undecorated._verbs) 988 return callback(*a, **kw) 989 990 return wrap 991 992 def error_callback(self, **kw): 993 self.process_origin() 994 995 996class JsonApiRequestPlugin(object): 997 ''' Ensures request content satisfies the OpenBMC json api format. ''' 998 name = 'json_api_request' 999 api = 2 1000 1001 error_str = "Expecting request format { 'data': <value> }, got '%s'" 1002 type_error_str = "Unsupported Content-Type: '%s'" 1003 json_type = "application/json" 1004 request_methods = ['PUT', 'POST', 'PATCH'] 1005 1006 @staticmethod 1007 def content_expected(): 1008 return request.method in JsonApiRequestPlugin.request_methods 1009 1010 def validate_request(self): 1011 if request.content_length > 0 and \ 1012 request.content_type != self.json_type: 1013 abort(415, self.type_error_str % request.content_type) 1014 1015 try: 1016 request.parameter_list = request.json.get('data') 1017 except ValueError, e: 1018 abort(400, str(e)) 1019 except (AttributeError, KeyError, TypeError): 1020 abort(400, self.error_str % request.json) 1021 1022 def apply(self, callback, route): 1023 content_type = getattr( 1024 route.get_undecorated_callback(), '_content_type', None) 1025 if self.json_type != content_type: 1026 return callback 1027 1028 verbs = getattr( 1029 route.get_undecorated_callback(), '_verbs', None) 1030 if verbs is None: 1031 return callback 1032 1033 if not set(self.request_methods).intersection(verbs): 1034 return callback 1035 1036 def wrap(*a, **kw): 1037 if self.content_expected(): 1038 self.validate_request() 1039 return callback(*a, **kw) 1040 1041 return wrap 1042 1043 1044class JsonApiRequestTypePlugin(object): 1045 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1046 name = 'json_api_method_request' 1047 api = 2 1048 1049 error_str = "Expecting request format { 'data': %s }, got '%s'" 1050 json_type = "application/json" 1051 1052 def apply(self, callback, route): 1053 content_type = getattr( 1054 route.get_undecorated_callback(), '_content_type', None) 1055 if self.json_type != content_type: 1056 return callback 1057 1058 request_type = getattr( 1059 route.get_undecorated_callback(), 'request_type', None) 1060 if request_type is None: 1061 return callback 1062 1063 def validate_request(): 1064 if not isinstance(request.parameter_list, request_type): 1065 abort(400, self.error_str % (str(request_type), request.json)) 1066 1067 def wrap(*a, **kw): 1068 if JsonApiRequestPlugin.content_expected(): 1069 validate_request() 1070 return callback(*a, **kw) 1071 1072 return wrap 1073 1074 1075class JsonErrorsPlugin(JSONPlugin): 1076 ''' Extend the Bottle JSONPlugin such that it also encodes error 1077 responses. ''' 1078 1079 def __init__(self, app, **kw): 1080 super(JsonErrorsPlugin, self).__init__(**kw) 1081 self.json_opts = { 1082 x: y for x, y in kw.iteritems() 1083 if x in ['indent', 'sort_keys']} 1084 app.install_error_callback(self.error_callback) 1085 1086 def error_callback(self, response_object, response_body, **kw): 1087 response_body['body'] = json.dumps(response_object, **self.json_opts) 1088 response.content_type = 'application/json' 1089 1090 1091class JsonApiResponsePlugin(object): 1092 ''' Emits responses in the OpenBMC json api format. ''' 1093 name = 'json_api_response' 1094 api = 2 1095 1096 @staticmethod 1097 def has_body(): 1098 return request.method not in ['OPTIONS'] 1099 1100 def __init__(self, app): 1101 app.install_error_callback(self.error_callback) 1102 1103 def apply(self, callback, route): 1104 skip = getattr( 1105 route.get_undecorated_callback(), 'suppress_json_resp', None) 1106 if skip: 1107 return callback 1108 1109 def wrap(*a, **kw): 1110 data = callback(*a, **kw) 1111 if self.has_body(): 1112 resp = {'data': data} 1113 resp['status'] = 'ok' 1114 resp['message'] = response.status_line 1115 return resp 1116 return wrap 1117 1118 def error_callback(self, error, response_object, **kw): 1119 response_object['message'] = error.status_line 1120 response_object['status'] = 'error' 1121 response_object.setdefault('data', {})['description'] = str(error.body) 1122 if error.status_code == 500: 1123 response_object['data']['exception'] = repr(error.exception) 1124 response_object['data']['traceback'] = error.traceback.splitlines() 1125 1126 1127class JsonpPlugin(object): 1128 ''' Json javascript wrapper. ''' 1129 name = 'jsonp' 1130 api = 2 1131 1132 def __init__(self, app, **kw): 1133 app.install_error_callback(self.error_callback) 1134 1135 @staticmethod 1136 def to_jsonp(json): 1137 jwrapper = request.query.callback or None 1138 if(jwrapper): 1139 response.set_header('Content-Type', 'application/javascript') 1140 json = jwrapper + '(' + json + ');' 1141 return json 1142 1143 def apply(self, callback, route): 1144 def wrap(*a, **kw): 1145 return self.to_jsonp(callback(*a, **kw)) 1146 return wrap 1147 1148 def error_callback(self, response_body, **kw): 1149 response_body['body'] = self.to_jsonp(response_body['body']) 1150 1151 1152class ContentCheckerPlugin(object): 1153 ''' Ensures that a route is associated with the expected content-type 1154 header. ''' 1155 name = 'content_checker' 1156 api = 2 1157 1158 class Checker: 1159 def __init__(self, type, callback): 1160 self.expected_type = type 1161 self.callback = callback 1162 self.error_str = "Expecting content type '%s', got '%s'" 1163 1164 def __call__(self, *a, **kw): 1165 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1166 self.expected_type and \ 1167 self.expected_type != request.content_type: 1168 abort(415, self.error_str % (self.expected_type, 1169 request.content_type)) 1170 1171 return self.callback(*a, **kw) 1172 1173 def apply(self, callback, route): 1174 content_type = getattr( 1175 route.get_undecorated_callback(), '_content_type', None) 1176 1177 return self.Checker(content_type, callback) 1178 1179 1180class App(Bottle): 1181 def __init__(self, **kw): 1182 super(App, self).__init__(autojson=False) 1183 1184 self.have_wsock = kw.get('have_wsock', False) 1185 1186 self.bus = dbus.SystemBus() 1187 self.mapper = obmc.mapper.Mapper(self.bus) 1188 self.error_callbacks = [] 1189 1190 self.install_hooks() 1191 self.install_plugins() 1192 self.create_handlers() 1193 self.install_handlers() 1194 1195 def install_plugins(self): 1196 # install json api plugins 1197 json_kw = {'indent': 2, 'sort_keys': True} 1198 self.install(AuthorizationPlugin()) 1199 self.install(CorsPlugin(self)) 1200 self.install(ContentCheckerPlugin()) 1201 self.install(JsonpPlugin(self, **json_kw)) 1202 self.install(JsonErrorsPlugin(self, **json_kw)) 1203 self.install(JsonApiResponsePlugin(self)) 1204 self.install(JsonApiRequestPlugin()) 1205 self.install(JsonApiRequestTypePlugin()) 1206 1207 def install_hooks(self): 1208 self.error_handler_type = type(self.default_error_handler) 1209 self.original_error_handler = self.default_error_handler 1210 self.default_error_handler = self.error_handler_type( 1211 self.custom_error_handler, self, Bottle) 1212 1213 self.real_router_match = self.router.match 1214 self.router.match = self.custom_router_match 1215 self.add_hook('before_request', self.strip_extra_slashes) 1216 1217 def create_handlers(self): 1218 # create route handlers 1219 self.session_handler = SessionHandler(self, self.bus) 1220 self.directory_handler = DirectoryHandler(self, self.bus) 1221 self.list_names_handler = ListNamesHandler(self, self.bus) 1222 self.list_handler = ListHandler(self, self.bus) 1223 self.method_handler = MethodHandler(self, self.bus) 1224 self.property_handler = PropertyHandler(self, self.bus) 1225 self.schema_handler = SchemaHandler(self, self.bus) 1226 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1227 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1228 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1229 if self.have_wsock: 1230 self.event_handler = EventHandler(self, self.bus) 1231 self.instance_handler = InstanceHandler(self, self.bus) 1232 1233 def install_handlers(self): 1234 self.session_handler.install() 1235 self.directory_handler.install() 1236 self.list_names_handler.install() 1237 self.list_handler.install() 1238 self.method_handler.install() 1239 self.property_handler.install() 1240 self.schema_handler.install() 1241 self.image_upload_post_handler.install() 1242 self.image_upload_put_handler.install() 1243 self.download_dump_get_handler.install() 1244 if self.have_wsock: 1245 self.event_handler.install() 1246 # this has to come last, since it matches everything 1247 self.instance_handler.install() 1248 1249 def install_error_callback(self, callback): 1250 self.error_callbacks.insert(0, callback) 1251 1252 def custom_router_match(self, environ): 1253 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1254 needed doesn't work for us since the instance rules match 1255 everything. This monkey-patch lets the route handler figure 1256 out which response is needed. This could be accomplished 1257 with a hook but that would require calling the router match 1258 function twice. 1259 ''' 1260 route, args = self.real_router_match(environ) 1261 if isinstance(route.callback, RouteHandler): 1262 route.callback._setup(**args) 1263 1264 return route, args 1265 1266 def custom_error_handler(self, res, error): 1267 ''' Allow plugins to modify error responses too via this custom 1268 error handler. ''' 1269 1270 response_object = {} 1271 response_body = {} 1272 for x in self.error_callbacks: 1273 x(error=error, 1274 response_object=response_object, 1275 response_body=response_body) 1276 1277 return response_body.get('body', "") 1278 1279 @staticmethod 1280 def strip_extra_slashes(): 1281 path = request.environ['PATH_INFO'] 1282 trailing = ("", "/")[path[-1] == '/'] 1283 parts = filter(bool, path.split('/')) 1284 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1285