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 busses = self.try_mapper_call( 332 self.mapper.get_object, path=path) 333 for items in busses.iteritems(): 334 m = self.find_method_on_bus(path, method, *items) 335 if m: 336 return m 337 338 abort(404, _4034_msg % ('method', 'found', method)) 339 340 def setup(self, path, method): 341 request.route_data['method'] = self.find(path, method) 342 343 def do_post(self, path, method): 344 try: 345 if request.parameter_list: 346 return request.route_data['method'](*request.parameter_list) 347 else: 348 return request.route_data['method']() 349 350 except dbus.exceptions.DBusException, e: 351 paramlist = [] 352 if e.get_dbus_name() == DBUS_INVALID_ARGS: 353 354 signature_list = get_method_signature(self.bus, self.service, 355 path, self.interface, 356 method) 357 if not signature_list: 358 abort(400, "Failed to get method signature: %s" % str(e)) 359 if len(signature_list) != len(request.parameter_list): 360 abort(400, "Invalid number of args") 361 converted_value = None 362 try: 363 for index, expected_type in enumerate(signature_list): 364 value = request.parameter_list[index] 365 converted_value = convert_type(expected_type, value) 366 paramlist.append(converted_value) 367 request.parameter_list = paramlist 368 self.do_post(path, method) 369 return 370 except Exception as ex: 371 abort(400, "Failed to convert the types") 372 abort(400, str(e)) 373 374 if e.get_dbus_name() == DBUS_TYPE_ERROR: 375 abort(400, str(e)) 376 raise 377 378 @staticmethod 379 def find_method_in_interface(method, obj, interface, methods): 380 if methods is None: 381 return None 382 383 method = obmc.utils.misc.find_case_insensitive(method, methods.keys()) 384 if method is not None: 385 iface = dbus.Interface(obj, interface) 386 return iface.get_dbus_method(method) 387 388 def find_method_on_bus(self, path, method, bus, interfaces): 389 obj = self.bus.get_object(bus, path, introspect=False) 390 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 391 data = iface.Introspect() 392 parser = IntrospectionNodeParser( 393 ElementTree.fromstring(data), 394 intf_match=obmc.utils.misc.ListMatch(interfaces)) 395 for x, y in parser.get_interfaces().iteritems(): 396 m = self.find_method_in_interface( 397 method, obj, x, y.get('method')) 398 if m: 399 self.service = bus 400 self.interface = x 401 return m 402 403 404class PropertyHandler(RouteHandler): 405 verbs = ['PUT', 'GET'] 406 rules = '<path:path>/attr/<prop>' 407 content_type = 'application/json' 408 409 def __init__(self, app, bus): 410 super(PropertyHandler, self).__init__( 411 app, bus, self.verbs, self.rules, self.content_type) 412 413 def find(self, path, prop): 414 self.app.instance_handler.setup(path) 415 obj = self.app.instance_handler.do_get(path) 416 real_name = obmc.utils.misc.find_case_insensitive( 417 prop, obj.keys()) 418 419 if not real_name: 420 if request.method == 'PUT': 421 abort(403, _4034_msg % ('property', 'created', prop)) 422 else: 423 abort(404, _4034_msg % ('property', 'found', prop)) 424 return real_name, {path: obj} 425 426 def setup(self, path, prop): 427 name, obj = self.find(path, prop) 428 request.route_data['obj'] = obj 429 request.route_data['name'] = name 430 431 def do_get(self, path, prop): 432 name = request.route_data['name'] 433 return request.route_data['obj'][path][name] 434 435 def do_put(self, path, prop, value=None): 436 if value is None: 437 value = request.parameter_list 438 439 prop, iface, properties_iface = self.get_host_interface( 440 path, prop, request.route_data['map'][path]) 441 try: 442 properties_iface.Set(iface, prop, value) 443 except ValueError, e: 444 abort(400, str(e)) 445 except dbus.exceptions.DBusException, e: 446 if e.get_dbus_name() == DBUS_INVALID_ARGS: 447 bus_name = properties_iface.bus_name 448 expected_type = get_type_signature_by_introspection(self.bus, 449 bus_name, 450 path, 451 prop) 452 if not expected_type: 453 abort(403, "Failed to get expected type: %s" % str(e)) 454 converted_value = None 455 try: 456 converted_value = convert_type(expected_type, value) 457 self.do_put(path, prop, converted_value) 458 return 459 except Exception as ex: 460 abort(403, "Failed to convert %s to type %s" % 461 (value, expected_type)) 462 abort(403, str(e)) 463 raise 464 465 def get_host_interface(self, path, prop, bus_info): 466 for bus, interfaces in bus_info.iteritems(): 467 obj = self.bus.get_object(bus, path, introspect=True) 468 properties_iface = dbus.Interface( 469 obj, dbus_interface=dbus.PROPERTIES_IFACE) 470 471 info = self.get_host_interface_on_bus( 472 path, prop, properties_iface, bus, interfaces) 473 if info is not None: 474 prop, iface = info 475 return prop, iface, properties_iface 476 477 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces): 478 for i in interfaces: 479 properties = self.try_properties_interface(iface.GetAll, i) 480 if not properties: 481 continue 482 match = obmc.utils.misc.find_case_insensitive( 483 prop, properties.keys()) 484 if match is None: 485 continue 486 prop = match 487 return prop, i 488 489 490class SchemaHandler(RouteHandler): 491 verbs = ['GET'] 492 rules = '<path:path>/schema' 493 494 def __init__(self, app, bus): 495 super(SchemaHandler, self).__init__( 496 app, bus, self.verbs, self.rules) 497 498 def find(self, path): 499 return self.try_mapper_call( 500 self.mapper.get_object, 501 path=path) 502 503 def setup(self, path): 504 request.route_data['map'] = self.find(path) 505 506 def do_get(self, path): 507 schema = {} 508 for x in request.route_data['map'].iterkeys(): 509 obj = self.bus.get_object(x, path, introspect=False) 510 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 511 data = iface.Introspect() 512 parser = IntrospectionNodeParser( 513 ElementTree.fromstring(data)) 514 for x, y in parser.get_interfaces().iteritems(): 515 schema[x] = y 516 517 return schema 518 519 520class InstanceHandler(RouteHandler): 521 verbs = ['GET', 'PUT', 'DELETE'] 522 rules = '<path:path>' 523 request_type = dict 524 525 def __init__(self, app, bus): 526 super(InstanceHandler, self).__init__( 527 app, bus, self.verbs, self.rules) 528 529 def find(self, path, callback=None): 530 return {path: self.try_mapper_call( 531 self.mapper.get_object, 532 callback, 533 path=path)} 534 535 def setup(self, path): 536 callback = None 537 if request.method == 'PUT': 538 def callback(e, **kw): 539 abort(403, _4034_msg % ('resource', 'created', path)) 540 541 if request.route_data.get('map') is None: 542 request.route_data['map'] = self.find(path, callback) 543 544 def do_get(self, path): 545 return self.mapper.enumerate_object( 546 path, 547 mapper_data=request.route_data['map']) 548 549 def do_put(self, path): 550 # make sure all properties exist in the request 551 obj = set(self.do_get(path).keys()) 552 req = set(request.parameter_list.keys()) 553 554 diff = list(obj.difference(req)) 555 if diff: 556 abort(403, _4034_msg % ( 557 'resource', 'removed', '%s/attr/%s' % (path, diff[0]))) 558 559 diff = list(req.difference(obj)) 560 if diff: 561 abort(403, _4034_msg % ( 562 'resource', 'created', '%s/attr/%s' % (path, diff[0]))) 563 564 for p, v in request.parameter_list.iteritems(): 565 self.app.property_handler.do_put( 566 path, p, v) 567 568 def do_delete(self, path): 569 for bus_info in request.route_data['map'][path].iteritems(): 570 if self.bus_missing_delete(path, *bus_info): 571 abort(403, _4034_msg % ('resource', 'removed', path)) 572 573 for bus in request.route_data['map'][path].iterkeys(): 574 self.delete_on_bus(path, bus) 575 576 def bus_missing_delete(self, path, bus, interfaces): 577 return DELETE_IFACE not in interfaces 578 579 def delete_on_bus(self, path, bus): 580 obj = self.bus.get_object(bus, path, introspect=False) 581 delete_iface = dbus.Interface( 582 obj, dbus_interface=DELETE_IFACE) 583 delete_iface.Delete() 584 585 586class SessionHandler(MethodHandler): 587 ''' Handles the /login and /logout routes, manages 588 server side session store and session cookies. ''' 589 590 rules = ['/login', '/logout'] 591 login_str = "User '%s' logged %s" 592 bad_passwd_str = "Invalid username or password" 593 no_user_str = "No user logged in" 594 bad_json_str = "Expecting request format { 'data': " \ 595 "[<username>, <password>] }, got '%s'" 596 _require_auth = None 597 MAX_SESSIONS = 16 598 599 def __init__(self, app, bus): 600 super(SessionHandler, self).__init__( 601 app, bus) 602 self.hmac_key = os.urandom(128) 603 self.session_store = [] 604 605 @staticmethod 606 def authenticate(username, clear): 607 try: 608 encoded = spwd.getspnam(username)[1] 609 return encoded == crypt.crypt(clear, encoded) 610 except KeyError: 611 return False 612 613 def invalidate_session(self, session): 614 try: 615 self.session_store.remove(session) 616 except ValueError: 617 pass 618 619 def new_session(self): 620 sid = os.urandom(32) 621 if self.MAX_SESSIONS <= len(self.session_store): 622 self.session_store.pop() 623 self.session_store.insert(0, {'sid': sid}) 624 625 return self.session_store[0] 626 627 def get_session(self, sid): 628 sids = [x['sid'] for x in self.session_store] 629 try: 630 return self.session_store[sids.index(sid)] 631 except ValueError: 632 return None 633 634 def get_session_from_cookie(self): 635 return self.get_session( 636 request.get_cookie( 637 'sid', secret=self.hmac_key)) 638 639 def do_post(self, **kw): 640 if request.path == '/login': 641 return self.do_login(**kw) 642 else: 643 return self.do_logout(**kw) 644 645 def do_logout(self, **kw): 646 session = self.get_session_from_cookie() 647 if session is not None: 648 user = session['user'] 649 self.invalidate_session(session) 650 response.delete_cookie('sid') 651 return self.login_str % (user, 'out') 652 653 return self.no_user_str 654 655 def do_login(self, **kw): 656 session = self.get_session_from_cookie() 657 if session is not None: 658 return self.login_str % (session['user'], 'in') 659 660 if len(request.parameter_list) != 2: 661 abort(400, self.bad_json_str % (request.json)) 662 663 if not self.authenticate(*request.parameter_list): 664 abort(401, self.bad_passwd_str) 665 666 user = request.parameter_list[0] 667 session = self.new_session() 668 session['user'] = user 669 response.set_cookie( 670 'sid', session['sid'], secret=self.hmac_key, 671 secure=True, 672 httponly=True) 673 return self.login_str % (user, 'in') 674 675 def find(self, **kw): 676 pass 677 678 def setup(self, **kw): 679 pass 680 681 682class ImageUploadUtils: 683 ''' Provides common utils for image upload. ''' 684 685 file_loc = '/tmp/images' 686 file_prefix = 'img' 687 file_suffix = '' 688 689 @classmethod 690 def do_upload(cls, filename=''): 691 if not os.path.exists(cls.file_loc): 692 os.makedirs(cls.file_loc) 693 if not filename: 694 handle, filename = tempfile.mkstemp(cls.file_suffix, 695 cls.file_prefix, cls.file_loc) 696 else: 697 filename = os.path.join(cls.file_loc, filename) 698 handle = os.open(filename, os.O_WRONLY | os.O_CREAT) 699 try: 700 file_contents = request.body.read() 701 request.body.close() 702 os.write(handle, file_contents) 703 except (IOError, ValueError), e: 704 abort(400, str(e)) 705 except: 706 abort(400, "Unexpected Error") 707 finally: 708 os.close(handle) 709 710 711class ImagePostHandler(RouteHandler): 712 ''' Handles the /upload/image route. ''' 713 714 verbs = ['POST'] 715 rules = ['/upload/image'] 716 content_type = 'application/octet-stream' 717 718 def __init__(self, app, bus): 719 super(ImagePostHandler, self).__init__( 720 app, bus, self.verbs, self.rules, self.content_type) 721 722 def do_post(self, filename=''): 723 ImageUploadUtils.do_upload() 724 725 def find(self, **kw): 726 pass 727 728 def setup(self, **kw): 729 pass 730 731 732class EventNotifier: 733 keyNames = {} 734 keyNames['event'] = 'event' 735 keyNames['path'] = 'path' 736 keyNames['intfMap'] = 'interfaces' 737 keyNames['propMap'] = 'properties' 738 keyNames['intf'] = 'interface' 739 740 def __init__(self, wsock, filters): 741 self.wsock = wsock 742 self.paths = filters.get("paths", []) 743 self.interfaces = filters.get("interfaces", []) 744 if not self.paths: 745 self.paths.append(None) 746 bus = dbus.SystemBus() 747 # Add a signal receiver for every path the client is interested in 748 for path in self.paths: 749 bus.add_signal_receiver( 750 self.interfaces_added_handler, 751 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 752 signal_name='InterfacesAdded', 753 path=path) 754 bus.add_signal_receiver( 755 self.properties_changed_handler, 756 dbus_interface=dbus.PROPERTIES_IFACE, 757 signal_name='PropertiesChanged', 758 path=path, 759 path_keyword='path') 760 loop = gobject.MainLoop() 761 # gobject's mainloop.run() will block the entire process, so the gevent 762 # scheduler and hence greenlets won't execute. The while-loop below 763 # works around this limitation by using gevent's sleep, instead of 764 # calling loop.run() 765 gcontext = loop.get_context() 766 while loop is not None: 767 try: 768 if gcontext.pending(): 769 gcontext.iteration() 770 else: 771 # gevent.sleep puts only the current greenlet to sleep, 772 # not the entire process. 773 gevent.sleep(5) 774 except WebSocketError: 775 break 776 777 def interfaces_added_handler(self, path, iprops, **kw): 778 ''' If the client is interested in these changes, respond to the 779 client. This handles d-bus interface additions.''' 780 if (not self.interfaces) or \ 781 (not set(iprops).isdisjoint(self.interfaces)): 782 response = {} 783 response[self.keyNames['event']] = "InterfacesAdded" 784 response[self.keyNames['path']] = path 785 response[self.keyNames['intfMap']] = iprops 786 try: 787 self.wsock.send(json.dumps(response)) 788 except WebSocketError: 789 return 790 791 def properties_changed_handler(self, interface, new, old, **kw): 792 ''' If the client is interested in these changes, respond to the 793 client. This handles d-bus property changes. ''' 794 if (not self.interfaces) or (interface in self.interfaces): 795 path = str(kw['path']) 796 response = {} 797 response[self.keyNames['event']] = "PropertiesChanged" 798 response[self.keyNames['path']] = path 799 response[self.keyNames['intf']] = interface 800 response[self.keyNames['propMap']] = new 801 try: 802 self.wsock.send(json.dumps(response)) 803 except WebSocketError: 804 return 805 806 807class EventHandler(RouteHandler): 808 ''' Handles the /subscribe route, for clients to be able 809 to subscribe to BMC events. ''' 810 811 verbs = ['GET'] 812 rules = ['/subscribe'] 813 814 def __init__(self, app, bus): 815 super(EventHandler, self).__init__( 816 app, bus, self.verbs, self.rules) 817 818 def find(self, **kw): 819 pass 820 821 def setup(self, **kw): 822 pass 823 824 def do_get(self): 825 wsock = request.environ.get('wsgi.websocket') 826 if not wsock: 827 abort(400, 'Expected WebSocket request.') 828 filters = wsock.receive() 829 filters = json.loads(filters) 830 notifier = EventNotifier(wsock, filters) 831 832 833class ImagePutHandler(RouteHandler): 834 ''' Handles the /upload/image/<filename> route. ''' 835 836 verbs = ['PUT'] 837 rules = ['/upload/image/<filename>'] 838 content_type = 'application/octet-stream' 839 840 def __init__(self, app, bus): 841 super(ImagePutHandler, self).__init__( 842 app, bus, self.verbs, self.rules, self.content_type) 843 844 def do_put(self, filename=''): 845 ImageUploadUtils.do_upload(filename) 846 847 def find(self, **kw): 848 pass 849 850 def setup(self, **kw): 851 pass 852 853 854class DownloadDumpHandler(RouteHandler): 855 ''' Handles the /download/dump route. ''' 856 857 verbs = 'GET' 858 rules = ['/download/dump/<dumpid>'] 859 content_type = 'application/octet-stream' 860 dump_loc = '/var/lib/phosphor-debug-collector/dumps' 861 suppress_json_resp = True 862 863 def __init__(self, app, bus): 864 super(DownloadDumpHandler, self).__init__( 865 app, bus, self.verbs, self.rules, self.content_type) 866 867 def do_get(self, dumpid): 868 return self.do_download(dumpid) 869 870 def find(self, **kw): 871 pass 872 873 def setup(self, **kw): 874 pass 875 876 def do_download(self, dumpid): 877 dump_loc = os.path.join(self.dump_loc, dumpid) 878 if not os.path.exists(dump_loc): 879 abort(404, "Path not found") 880 881 files = os.listdir(dump_loc) 882 num_files = len(files) 883 if num_files == 0: 884 abort(404, "Dump not found") 885 886 return static_file(os.path.basename(files[0]), root=dump_loc, 887 download=True, mimetype=self.content_type) 888 889 890class AuthorizationPlugin(object): 891 ''' Invokes an optional list of authorization callbacks. ''' 892 893 name = 'authorization' 894 api = 2 895 896 class Compose: 897 def __init__(self, validators, callback, session_mgr): 898 self.validators = validators 899 self.callback = callback 900 self.session_mgr = session_mgr 901 902 def __call__(self, *a, **kw): 903 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) 904 session = self.session_mgr.get_session(sid) 905 if request.method != 'OPTIONS': 906 for x in self.validators: 907 x(session, *a, **kw) 908 909 return self.callback(*a, **kw) 910 911 def apply(self, callback, route): 912 undecorated = route.get_undecorated_callback() 913 if not isinstance(undecorated, RouteHandler): 914 return callback 915 916 auth_types = getattr( 917 undecorated, '_require_auth', None) 918 if not auth_types: 919 return callback 920 921 return self.Compose( 922 auth_types, callback, undecorated.app.session_handler) 923 924 925class CorsPlugin(object): 926 ''' Add CORS headers. ''' 927 928 name = 'cors' 929 api = 2 930 931 @staticmethod 932 def process_origin(): 933 origin = request.headers.get('Origin') 934 if origin: 935 response.add_header('Access-Control-Allow-Origin', origin) 936 response.add_header( 937 'Access-Control-Allow-Credentials', 'true') 938 939 @staticmethod 940 def process_method_and_headers(verbs): 941 method = request.headers.get('Access-Control-Request-Method') 942 headers = request.headers.get('Access-Control-Request-Headers') 943 if headers: 944 headers = [x.lower() for x in headers.split(',')] 945 946 if method in verbs \ 947 and headers == ['content-type']: 948 response.add_header('Access-Control-Allow-Methods', method) 949 response.add_header( 950 'Access-Control-Allow-Headers', 'Content-Type') 951 952 def __init__(self, app): 953 app.install_error_callback(self.error_callback) 954 955 def apply(self, callback, route): 956 undecorated = route.get_undecorated_callback() 957 if not isinstance(undecorated, RouteHandler): 958 return callback 959 960 if not getattr(undecorated, '_enable_cors', None): 961 return callback 962 963 def wrap(*a, **kw): 964 self.process_origin() 965 self.process_method_and_headers(undecorated._verbs) 966 return callback(*a, **kw) 967 968 return wrap 969 970 def error_callback(self, **kw): 971 self.process_origin() 972 973 974class JsonApiRequestPlugin(object): 975 ''' Ensures request content satisfies the OpenBMC json api format. ''' 976 name = 'json_api_request' 977 api = 2 978 979 error_str = "Expecting request format { 'data': <value> }, got '%s'" 980 type_error_str = "Unsupported Content-Type: '%s'" 981 json_type = "application/json" 982 request_methods = ['PUT', 'POST', 'PATCH'] 983 984 @staticmethod 985 def content_expected(): 986 return request.method in JsonApiRequestPlugin.request_methods 987 988 def validate_request(self): 989 if request.content_length > 0 and \ 990 request.content_type != self.json_type: 991 abort(415, self.type_error_str % request.content_type) 992 993 try: 994 request.parameter_list = request.json.get('data') 995 except ValueError, e: 996 abort(400, str(e)) 997 except (AttributeError, KeyError, TypeError): 998 abort(400, self.error_str % request.json) 999 1000 def apply(self, callback, route): 1001 content_type = getattr( 1002 route.get_undecorated_callback(), '_content_type', None) 1003 if self.json_type != content_type: 1004 return callback 1005 1006 verbs = getattr( 1007 route.get_undecorated_callback(), '_verbs', None) 1008 if verbs is None: 1009 return callback 1010 1011 if not set(self.request_methods).intersection(verbs): 1012 return callback 1013 1014 def wrap(*a, **kw): 1015 if self.content_expected(): 1016 self.validate_request() 1017 return callback(*a, **kw) 1018 1019 return wrap 1020 1021 1022class JsonApiRequestTypePlugin(object): 1023 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1024 name = 'json_api_method_request' 1025 api = 2 1026 1027 error_str = "Expecting request format { 'data': %s }, got '%s'" 1028 json_type = "application/json" 1029 1030 def apply(self, callback, route): 1031 content_type = getattr( 1032 route.get_undecorated_callback(), '_content_type', None) 1033 if self.json_type != content_type: 1034 return callback 1035 1036 request_type = getattr( 1037 route.get_undecorated_callback(), 'request_type', None) 1038 if request_type is None: 1039 return callback 1040 1041 def validate_request(): 1042 if not isinstance(request.parameter_list, request_type): 1043 abort(400, self.error_str % (str(request_type), request.json)) 1044 1045 def wrap(*a, **kw): 1046 if JsonApiRequestPlugin.content_expected(): 1047 validate_request() 1048 return callback(*a, **kw) 1049 1050 return wrap 1051 1052 1053class JsonErrorsPlugin(JSONPlugin): 1054 ''' Extend the Bottle JSONPlugin such that it also encodes error 1055 responses. ''' 1056 1057 def __init__(self, app, **kw): 1058 super(JsonErrorsPlugin, self).__init__(**kw) 1059 self.json_opts = { 1060 x: y for x, y in kw.iteritems() 1061 if x in ['indent', 'sort_keys']} 1062 app.install_error_callback(self.error_callback) 1063 1064 def error_callback(self, response_object, response_body, **kw): 1065 response_body['body'] = json.dumps(response_object, **self.json_opts) 1066 response.content_type = 'application/json' 1067 1068 1069class JsonApiResponsePlugin(object): 1070 ''' Emits responses in the OpenBMC json api format. ''' 1071 name = 'json_api_response' 1072 api = 2 1073 1074 @staticmethod 1075 def has_body(): 1076 return request.method not in ['OPTIONS'] 1077 1078 def __init__(self, app): 1079 app.install_error_callback(self.error_callback) 1080 1081 def apply(self, callback, route): 1082 skip = getattr( 1083 route.get_undecorated_callback(), 'suppress_json_resp', None) 1084 if skip: 1085 return callback 1086 1087 def wrap(*a, **kw): 1088 data = callback(*a, **kw) 1089 if self.has_body(): 1090 resp = {'data': data} 1091 resp['status'] = 'ok' 1092 resp['message'] = response.status_line 1093 return resp 1094 return wrap 1095 1096 def error_callback(self, error, response_object, **kw): 1097 response_object['message'] = error.status_line 1098 response_object['status'] = 'error' 1099 response_object.setdefault('data', {})['description'] = str(error.body) 1100 if error.status_code == 500: 1101 response_object['data']['exception'] = repr(error.exception) 1102 response_object['data']['traceback'] = error.traceback.splitlines() 1103 1104 1105class JsonpPlugin(object): 1106 ''' Json javascript wrapper. ''' 1107 name = 'jsonp' 1108 api = 2 1109 1110 def __init__(self, app, **kw): 1111 app.install_error_callback(self.error_callback) 1112 1113 @staticmethod 1114 def to_jsonp(json): 1115 jwrapper = request.query.callback or None 1116 if(jwrapper): 1117 response.set_header('Content-Type', 'application/javascript') 1118 json = jwrapper + '(' + json + ');' 1119 return json 1120 1121 def apply(self, callback, route): 1122 def wrap(*a, **kw): 1123 return self.to_jsonp(callback(*a, **kw)) 1124 return wrap 1125 1126 def error_callback(self, response_body, **kw): 1127 response_body['body'] = self.to_jsonp(response_body['body']) 1128 1129 1130class ContentCheckerPlugin(object): 1131 ''' Ensures that a route is associated with the expected content-type 1132 header. ''' 1133 name = 'content_checker' 1134 api = 2 1135 1136 class Checker: 1137 def __init__(self, type, callback): 1138 self.expected_type = type 1139 self.callback = callback 1140 self.error_str = "Expecting content type '%s', got '%s'" 1141 1142 def __call__(self, *a, **kw): 1143 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1144 self.expected_type and \ 1145 self.expected_type != request.content_type: 1146 abort(415, self.error_str % (self.expected_type, 1147 request.content_type)) 1148 1149 return self.callback(*a, **kw) 1150 1151 def apply(self, callback, route): 1152 content_type = getattr( 1153 route.get_undecorated_callback(), '_content_type', None) 1154 1155 return self.Checker(content_type, callback) 1156 1157 1158class App(Bottle): 1159 def __init__(self, **kw): 1160 super(App, self).__init__(autojson=False) 1161 1162 self.have_wsock = kw.get('have_wsock', False) 1163 1164 self.bus = dbus.SystemBus() 1165 self.mapper = obmc.mapper.Mapper(self.bus) 1166 self.error_callbacks = [] 1167 1168 self.install_hooks() 1169 self.install_plugins() 1170 self.create_handlers() 1171 self.install_handlers() 1172 1173 def install_plugins(self): 1174 # install json api plugins 1175 json_kw = {'indent': 2, 'sort_keys': True} 1176 self.install(AuthorizationPlugin()) 1177 self.install(CorsPlugin(self)) 1178 self.install(ContentCheckerPlugin()) 1179 self.install(JsonpPlugin(self, **json_kw)) 1180 self.install(JsonErrorsPlugin(self, **json_kw)) 1181 self.install(JsonApiResponsePlugin(self)) 1182 self.install(JsonApiRequestPlugin()) 1183 self.install(JsonApiRequestTypePlugin()) 1184 1185 def install_hooks(self): 1186 self.error_handler_type = type(self.default_error_handler) 1187 self.original_error_handler = self.default_error_handler 1188 self.default_error_handler = self.error_handler_type( 1189 self.custom_error_handler, self, Bottle) 1190 1191 self.real_router_match = self.router.match 1192 self.router.match = self.custom_router_match 1193 self.add_hook('before_request', self.strip_extra_slashes) 1194 1195 def create_handlers(self): 1196 # create route handlers 1197 self.session_handler = SessionHandler(self, self.bus) 1198 self.directory_handler = DirectoryHandler(self, self.bus) 1199 self.list_names_handler = ListNamesHandler(self, self.bus) 1200 self.list_handler = ListHandler(self, self.bus) 1201 self.method_handler = MethodHandler(self, self.bus) 1202 self.property_handler = PropertyHandler(self, self.bus) 1203 self.schema_handler = SchemaHandler(self, self.bus) 1204 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1205 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1206 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1207 if self.have_wsock: 1208 self.event_handler = EventHandler(self, self.bus) 1209 self.instance_handler = InstanceHandler(self, self.bus) 1210 1211 def install_handlers(self): 1212 self.session_handler.install() 1213 self.directory_handler.install() 1214 self.list_names_handler.install() 1215 self.list_handler.install() 1216 self.method_handler.install() 1217 self.property_handler.install() 1218 self.schema_handler.install() 1219 self.image_upload_post_handler.install() 1220 self.image_upload_put_handler.install() 1221 self.download_dump_get_handler.install() 1222 if self.have_wsock: 1223 self.event_handler.install() 1224 # this has to come last, since it matches everything 1225 self.instance_handler.install() 1226 1227 def install_error_callback(self, callback): 1228 self.error_callbacks.insert(0, callback) 1229 1230 def custom_router_match(self, environ): 1231 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1232 needed doesn't work for us since the instance rules match 1233 everything. This monkey-patch lets the route handler figure 1234 out which response is needed. This could be accomplished 1235 with a hook but that would require calling the router match 1236 function twice. 1237 ''' 1238 route, args = self.real_router_match(environ) 1239 if isinstance(route.callback, RouteHandler): 1240 route.callback._setup(**args) 1241 1242 return route, args 1243 1244 def custom_error_handler(self, res, error): 1245 ''' Allow plugins to modify error responses too via this custom 1246 error handler. ''' 1247 1248 response_object = {} 1249 response_body = {} 1250 for x in self.error_callbacks: 1251 x(error=error, 1252 response_object=response_object, 1253 response_body=response_body) 1254 1255 return response_body.get('body', "") 1256 1257 @staticmethod 1258 def strip_extra_slashes(): 1259 path = request.environ['PATH_INFO'] 1260 trailing = ("", "/")[path[-1] == '/'] 1261 parts = filter(bool, path.split('/')) 1262 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1263