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