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