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