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 response.add_header('X-Frame-Options', 'deny') 970 response.add_header('X-Content-Type-Options', 'nosniff') 971 response.add_header('X-XSS-Protection', '1; mode=block') 972 response.add_header( 973 'Content-Security-Policy', "default-src 'self'") 974 response.add_header( 975 'Strict-Transport-Security', 976 'max-age=31536000; includeSubDomains; preload') 977 978 def __init__(self, app): 979 app.install_error_callback(self.error_callback) 980 981 def apply(self, callback, route): 982 undecorated = route.get_undecorated_callback() 983 if not isinstance(undecorated, RouteHandler): 984 return callback 985 986 if not getattr(undecorated, '_enable_cors', None): 987 return callback 988 989 def wrap(*a, **kw): 990 self.process_origin() 991 self.process_method_and_headers(undecorated._verbs) 992 return callback(*a, **kw) 993 994 return wrap 995 996 def error_callback(self, **kw): 997 self.process_origin() 998 999 1000class JsonApiRequestPlugin(object): 1001 ''' Ensures request content satisfies the OpenBMC json api format. ''' 1002 name = 'json_api_request' 1003 api = 2 1004 1005 error_str = "Expecting request format { 'data': <value> }, got '%s'" 1006 type_error_str = "Unsupported Content-Type: '%s'" 1007 json_type = "application/json" 1008 request_methods = ['PUT', 'POST', 'PATCH'] 1009 1010 @staticmethod 1011 def content_expected(): 1012 return request.method in JsonApiRequestPlugin.request_methods 1013 1014 def validate_request(self): 1015 if request.content_length > 0 and \ 1016 request.content_type != self.json_type: 1017 abort(415, self.type_error_str % request.content_type) 1018 1019 try: 1020 request.parameter_list = request.json.get('data') 1021 except ValueError, e: 1022 abort(400, str(e)) 1023 except (AttributeError, KeyError, TypeError): 1024 abort(400, self.error_str % request.json) 1025 1026 def apply(self, callback, route): 1027 content_type = getattr( 1028 route.get_undecorated_callback(), '_content_type', None) 1029 if self.json_type != content_type: 1030 return callback 1031 1032 verbs = getattr( 1033 route.get_undecorated_callback(), '_verbs', None) 1034 if verbs is None: 1035 return callback 1036 1037 if not set(self.request_methods).intersection(verbs): 1038 return callback 1039 1040 def wrap(*a, **kw): 1041 if self.content_expected(): 1042 self.validate_request() 1043 return callback(*a, **kw) 1044 1045 return wrap 1046 1047 1048class JsonApiRequestTypePlugin(object): 1049 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1050 name = 'json_api_method_request' 1051 api = 2 1052 1053 error_str = "Expecting request format { 'data': %s }, got '%s'" 1054 json_type = "application/json" 1055 1056 def apply(self, callback, route): 1057 content_type = getattr( 1058 route.get_undecorated_callback(), '_content_type', None) 1059 if self.json_type != content_type: 1060 return callback 1061 1062 request_type = getattr( 1063 route.get_undecorated_callback(), 'request_type', None) 1064 if request_type is None: 1065 return callback 1066 1067 def validate_request(): 1068 if not isinstance(request.parameter_list, request_type): 1069 abort(400, self.error_str % (str(request_type), request.json)) 1070 1071 def wrap(*a, **kw): 1072 if JsonApiRequestPlugin.content_expected(): 1073 validate_request() 1074 return callback(*a, **kw) 1075 1076 return wrap 1077 1078 1079class JsonErrorsPlugin(JSONPlugin): 1080 ''' Extend the Bottle JSONPlugin such that it also encodes error 1081 responses. ''' 1082 1083 def __init__(self, app, **kw): 1084 super(JsonErrorsPlugin, self).__init__(**kw) 1085 self.json_opts = { 1086 x: y for x, y in kw.iteritems() 1087 if x in ['indent', 'sort_keys']} 1088 app.install_error_callback(self.error_callback) 1089 1090 def error_callback(self, response_object, response_body, **kw): 1091 response_body['body'] = json.dumps(response_object, **self.json_opts) 1092 response.content_type = 'application/json' 1093 1094 1095class JsonApiResponsePlugin(object): 1096 ''' Emits responses in the OpenBMC json api format. ''' 1097 name = 'json_api_response' 1098 api = 2 1099 1100 @staticmethod 1101 def has_body(): 1102 return request.method not in ['OPTIONS'] 1103 1104 def __init__(self, app): 1105 app.install_error_callback(self.error_callback) 1106 1107 def apply(self, callback, route): 1108 skip = getattr( 1109 route.get_undecorated_callback(), 'suppress_json_resp', None) 1110 if skip: 1111 return callback 1112 1113 def wrap(*a, **kw): 1114 data = callback(*a, **kw) 1115 if self.has_body(): 1116 resp = {'data': data} 1117 resp['status'] = 'ok' 1118 resp['message'] = response.status_line 1119 return resp 1120 return wrap 1121 1122 def error_callback(self, error, response_object, **kw): 1123 response_object['message'] = error.status_line 1124 response_object['status'] = 'error' 1125 response_object.setdefault('data', {})['description'] = str(error.body) 1126 if error.status_code == 500: 1127 response_object['data']['exception'] = repr(error.exception) 1128 response_object['data']['traceback'] = error.traceback.splitlines() 1129 1130 1131class JsonpPlugin(object): 1132 ''' Json javascript wrapper. ''' 1133 name = 'jsonp' 1134 api = 2 1135 1136 def __init__(self, app, **kw): 1137 app.install_error_callback(self.error_callback) 1138 1139 @staticmethod 1140 def to_jsonp(json): 1141 jwrapper = request.query.callback or None 1142 if(jwrapper): 1143 response.set_header('Content-Type', 'application/javascript') 1144 json = jwrapper + '(' + json + ');' 1145 return json 1146 1147 def apply(self, callback, route): 1148 def wrap(*a, **kw): 1149 return self.to_jsonp(callback(*a, **kw)) 1150 return wrap 1151 1152 def error_callback(self, response_body, **kw): 1153 response_body['body'] = self.to_jsonp(response_body['body']) 1154 1155 1156class ContentCheckerPlugin(object): 1157 ''' Ensures that a route is associated with the expected content-type 1158 header. ''' 1159 name = 'content_checker' 1160 api = 2 1161 1162 class Checker: 1163 def __init__(self, type, callback): 1164 self.expected_type = type 1165 self.callback = callback 1166 self.error_str = "Expecting content type '%s', got '%s'" 1167 1168 def __call__(self, *a, **kw): 1169 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1170 self.expected_type and \ 1171 self.expected_type != request.content_type: 1172 abort(415, self.error_str % (self.expected_type, 1173 request.content_type)) 1174 1175 return self.callback(*a, **kw) 1176 1177 def apply(self, callback, route): 1178 content_type = getattr( 1179 route.get_undecorated_callback(), '_content_type', None) 1180 1181 return self.Checker(content_type, callback) 1182 1183 1184class App(Bottle): 1185 def __init__(self, **kw): 1186 super(App, self).__init__(autojson=False) 1187 1188 self.have_wsock = kw.get('have_wsock', False) 1189 1190 self.bus = dbus.SystemBus() 1191 self.mapper = obmc.mapper.Mapper(self.bus) 1192 self.error_callbacks = [] 1193 1194 self.install_hooks() 1195 self.install_plugins() 1196 self.create_handlers() 1197 self.install_handlers() 1198 1199 def install_plugins(self): 1200 # install json api plugins 1201 json_kw = {'indent': 2, 'sort_keys': True} 1202 self.install(AuthorizationPlugin()) 1203 self.install(CorsPlugin(self)) 1204 self.install(ContentCheckerPlugin()) 1205 self.install(JsonpPlugin(self, **json_kw)) 1206 self.install(JsonErrorsPlugin(self, **json_kw)) 1207 self.install(JsonApiResponsePlugin(self)) 1208 self.install(JsonApiRequestPlugin()) 1209 self.install(JsonApiRequestTypePlugin()) 1210 1211 def install_hooks(self): 1212 self.error_handler_type = type(self.default_error_handler) 1213 self.original_error_handler = self.default_error_handler 1214 self.default_error_handler = self.error_handler_type( 1215 self.custom_error_handler, self, Bottle) 1216 1217 self.real_router_match = self.router.match 1218 self.router.match = self.custom_router_match 1219 self.add_hook('before_request', self.strip_extra_slashes) 1220 1221 def create_handlers(self): 1222 # create route handlers 1223 self.session_handler = SessionHandler(self, self.bus) 1224 self.directory_handler = DirectoryHandler(self, self.bus) 1225 self.list_names_handler = ListNamesHandler(self, self.bus) 1226 self.list_handler = ListHandler(self, self.bus) 1227 self.method_handler = MethodHandler(self, self.bus) 1228 self.property_handler = PropertyHandler(self, self.bus) 1229 self.schema_handler = SchemaHandler(self, self.bus) 1230 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1231 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1232 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1233 if self.have_wsock: 1234 self.event_handler = EventHandler(self, self.bus) 1235 self.instance_handler = InstanceHandler(self, self.bus) 1236 1237 def install_handlers(self): 1238 self.session_handler.install() 1239 self.directory_handler.install() 1240 self.list_names_handler.install() 1241 self.list_handler.install() 1242 self.method_handler.install() 1243 self.property_handler.install() 1244 self.schema_handler.install() 1245 self.image_upload_post_handler.install() 1246 self.image_upload_put_handler.install() 1247 self.download_dump_get_handler.install() 1248 if self.have_wsock: 1249 self.event_handler.install() 1250 # this has to come last, since it matches everything 1251 self.instance_handler.install() 1252 1253 def install_error_callback(self, callback): 1254 self.error_callbacks.insert(0, callback) 1255 1256 def custom_router_match(self, environ): 1257 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1258 needed doesn't work for us since the instance rules match 1259 everything. This monkey-patch lets the route handler figure 1260 out which response is needed. This could be accomplished 1261 with a hook but that would require calling the router match 1262 function twice. 1263 ''' 1264 route, args = self.real_router_match(environ) 1265 if isinstance(route.callback, RouteHandler): 1266 route.callback._setup(**args) 1267 1268 return route, args 1269 1270 def custom_error_handler(self, res, error): 1271 ''' Allow plugins to modify error responses too via this custom 1272 error handler. ''' 1273 1274 response_object = {} 1275 response_body = {} 1276 for x in self.error_callbacks: 1277 x(error=error, 1278 response_object=response_object, 1279 response_body=response_body) 1280 1281 return response_body.get('body', "") 1282 1283 @staticmethod 1284 def strip_extra_slashes(): 1285 path = request.environ['PATH_INFO'] 1286 trailing = ("", "/")[path[-1] == '/'] 1287 parts = filter(bool, path.split('/')) 1288 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1289