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 sys 19import dbus 20import dbus.exceptions 21import json 22from xml.etree import ElementTree 23from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError 24from bottle import static_file 25import obmc.utils.misc 26from obmc.dbuslib.introspection import IntrospectionNodeParser 27import obmc.mapper 28import spwd 29import grp 30import crypt 31import tempfile 32import re 33import mimetypes 34import fnmatch 35have_wsock = True 36try: 37 from geventwebsocket import WebSocketError 38except ImportError: 39 have_wsock = False 40if have_wsock: 41 from dbus.mainloop.glib import DBusGMainLoop 42 DBusGMainLoop(set_as_default=True) 43 # TODO: openbmc/openbmc#2994 remove python 2 support 44 try: # python 2 45 import gobject 46 except ImportError: # python 3 47 from gi.repository import GObject as gobject 48 import gevent 49 from gevent import socket 50 from gevent import Greenlet 51 52DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface' 53DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' 54DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs' 55DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError' 56DELETE_IFACE = 'xyz.openbmc_project.Object.Delete' 57SOFTWARE_PATH = '/xyz/openbmc_project/software' 58WEBSOCKET_TIMEOUT = 45 59 60_4034_msg = "The specified %s cannot be %s: '%s'" 61 62www_base_path = '/usr/share/www/' 63 64 65def valid_user(session, *a, **kw): 66 ''' Authorization plugin callback that checks 67 that the user is logged in. ''' 68 if session is None: 69 abort(401, 'Login required') 70 71 72def get_type_signature_by_introspection(bus, service, object_path, 73 property_name): 74 obj = bus.get_object(service, object_path) 75 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable') 76 xml_string = iface.Introspect() 77 for child in ElementTree.fromstring(xml_string): 78 # Iterate over each interfaces's properties to find 79 # matching property_name, and return its signature string 80 if child.tag == 'interface': 81 for i in child.iter(): 82 if ('name' in i.attrib) and \ 83 (i.attrib['name'] == property_name): 84 type_signature = i.attrib['type'] 85 return type_signature 86 87 88def get_method_signature(bus, service, object_path, interface, method): 89 obj = bus.get_object(service, object_path) 90 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable') 91 xml_string = iface.Introspect() 92 arglist = [] 93 94 root = ElementTree.fromstring(xml_string) 95 for dbus_intf in root.findall('interface'): 96 if (dbus_intf.get('name') == interface): 97 for dbus_method in dbus_intf.findall('method'): 98 if(dbus_method.get('name') == method): 99 for arg in dbus_method.findall('arg'): 100 arglist.append(arg.get('type')) 101 return arglist 102 103 104def split_struct_signature(signature): 105 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?' 106 struct_matches = re.findall(struct_regex, signature) 107 return struct_matches 108 109 110def convert_type(signature, value): 111 # Basic Types 112 converted_value = None 113 converted_container = None 114 # TODO: openbmc/openbmc#2994 remove python 2 support 115 try: # python 2 116 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int, 117 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32, 118 't': dbus.UInt64, 'd': float, 's': str} 119 except NameError: # python 3 120 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int, 121 'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32, 122 't': dbus.UInt64, 'd': float, 's': str} 123 array_matches = re.match(r'a\((\S+)\)', signature) 124 struct_matches = re.match(r'\((\S+)\)', signature) 125 dictionary_matches = re.match(r'a{(\S+)}', signature) 126 if signature in basic_types: 127 converted_value = basic_types[signature](value) 128 return converted_value 129 # Array 130 if array_matches: 131 element_type = array_matches.group(1) 132 converted_container = list() 133 # Test if value is a list 134 # to avoid iterating over each character in a string. 135 # Iterate over each item and convert type 136 if isinstance(value, list): 137 for i in value: 138 converted_element = convert_type(element_type, i) 139 converted_container.append(converted_element) 140 # Convert non-sequence to expected type, and append to list 141 else: 142 converted_element = convert_type(element_type, value) 143 converted_container.append(converted_element) 144 return converted_container 145 # Struct 146 if struct_matches: 147 element_types = struct_matches.group(1) 148 split_element_types = split_struct_signature(element_types) 149 converted_container = list() 150 # Test if value is a list 151 if isinstance(value, list): 152 for index, val in enumerate(value): 153 converted_element = convert_type(split_element_types[index], 154 value[index]) 155 converted_container.append(converted_element) 156 else: 157 converted_element = convert_type(element_types, value) 158 converted_container.append(converted_element) 159 return tuple(converted_container) 160 # Dictionary 161 if dictionary_matches: 162 element_types = dictionary_matches.group(1) 163 split_element_types = split_struct_signature(element_types) 164 converted_container = dict() 165 # Convert each element of dict 166 for key, val in value.items(): 167 converted_key = convert_type(split_element_types[0], key) 168 converted_val = convert_type(split_element_types[1], val) 169 converted_container[converted_key] = converted_val 170 return converted_container 171 172 173def send_ws_ping(wsock, timeout) : 174 # Most webservers close websockets after 60 seconds of 175 # inactivity. Make sure to send a ping before that. 176 payload = "ping" 177 # the ping payload can be anything, the receiver has to just 178 # return the same back. 179 while True: 180 gevent.sleep(timeout) 181 try: 182 if wsock: 183 wsock.send_frame(payload, wsock.OPCODE_PING) 184 except Exception as e: 185 wsock.close() 186 return 187 188 189class UserInGroup: 190 ''' Authorization plugin callback that checks that the user is logged in 191 and a member of a group. ''' 192 def __init__(self, group): 193 self.group = group 194 195 def __call__(self, session, *a, **kw): 196 valid_user(session, *a, **kw) 197 res = False 198 199 try: 200 res = session['user'] in grp.getgrnam(self.group)[3] 201 except KeyError: 202 pass 203 204 if not res: 205 abort(403, 'Insufficient access') 206 207 208class RouteHandler(object): 209 _require_auth = obmc.utils.misc.makelist(valid_user) 210 _enable_cors = True 211 212 def __init__(self, app, bus, verbs, rules, content_type=''): 213 self.app = app 214 self.bus = bus 215 self.mapper = obmc.mapper.Mapper(bus) 216 self._verbs = obmc.utils.misc.makelist(verbs) 217 self._rules = rules 218 self._content_type = content_type 219 220 if 'GET' in self._verbs: 221 self._verbs = list(set(self._verbs + ['HEAD'])) 222 if 'OPTIONS' not in self._verbs: 223 self._verbs.append('OPTIONS') 224 225 def _setup(self, **kw): 226 request.route_data = {} 227 228 if request.method in self._verbs: 229 if request.method != 'OPTIONS': 230 return self.setup(**kw) 231 232 # Javascript implementations will not send credentials 233 # with an OPTIONS request. Don't help malicious clients 234 # by checking the path here and returning a 404 if the 235 # path doesn't exist. 236 return None 237 238 # Return 405 239 raise HTTPError( 240 405, "Method not allowed.", Allow=','.join(self._verbs)) 241 242 def __call__(self, **kw): 243 return getattr(self, 'do_' + request.method.lower())(**kw) 244 245 def do_head(self, **kw): 246 return self.do_get(**kw) 247 248 def do_options(self, **kw): 249 for v in self._verbs: 250 response.set_header( 251 'Allow', 252 ','.join(self._verbs)) 253 return None 254 255 def install(self): 256 self.app.route( 257 self._rules, callback=self, 258 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE']) 259 260 @staticmethod 261 def try_mapper_call(f, callback=None, **kw): 262 try: 263 return f(**kw) 264 except dbus.exceptions.DBusException as e: 265 if e.get_dbus_name() == \ 266 'org.freedesktop.DBus.Error.ObjectPathInUse': 267 abort(503, str(e)) 268 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND: 269 raise 270 if callback is None: 271 def callback(e, **kw): 272 abort(404, str(e)) 273 274 callback(e, **kw) 275 276 @staticmethod 277 def try_properties_interface(f, *a): 278 try: 279 return f(*a) 280 except dbus.exceptions.DBusException as e: 281 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name(): 282 # interface doesn't have any properties 283 return None 284 if DBUS_UNKNOWN_METHOD == e.get_dbus_name(): 285 # properties interface not implemented at all 286 return None 287 raise 288 289 290class DirectoryHandler(RouteHandler): 291 verbs = 'GET' 292 rules = '<path:path>/' 293 294 def __init__(self, app, bus): 295 super(DirectoryHandler, self).__init__( 296 app, bus, self.verbs, self.rules) 297 298 def find(self, path='/'): 299 return self.try_mapper_call( 300 self.mapper.get_subtree_paths, path=path, depth=1) 301 302 def setup(self, path='/'): 303 request.route_data['map'] = self.find(path) 304 305 def do_get(self, path='/'): 306 return request.route_data['map'] 307 308 309class ListNamesHandler(RouteHandler): 310 verbs = 'GET' 311 rules = ['/list', '<path:path>/list'] 312 313 def __init__(self, app, bus): 314 super(ListNamesHandler, self).__init__( 315 app, bus, self.verbs, self.rules) 316 317 def find(self, path='/'): 318 return list(self.try_mapper_call( 319 self.mapper.get_subtree, path=path).keys()) 320 321 def setup(self, path='/'): 322 request.route_data['map'] = self.find(path) 323 324 def do_get(self, path='/'): 325 return request.route_data['map'] 326 327 328class ListHandler(RouteHandler): 329 verbs = 'GET' 330 rules = ['/enumerate', '<path:path>/enumerate'] 331 332 def __init__(self, app, bus): 333 super(ListHandler, self).__init__( 334 app, bus, self.verbs, self.rules) 335 336 def find(self, path='/'): 337 return self.try_mapper_call( 338 self.mapper.get_subtree, path=path) 339 340 def setup(self, path='/'): 341 request.route_data['map'] = self.find(path) 342 343 def do_get(self, path='/'): 344 return {x: y for x, y in self.mapper.enumerate_subtree( 345 path, 346 mapper_data=request.route_data['map']).dataitems()} 347 348 349class MethodHandler(RouteHandler): 350 verbs = 'POST' 351 rules = '<path:path>/action/<method>' 352 request_type = list 353 content_type = 'application/json' 354 355 def __init__(self, app, bus): 356 super(MethodHandler, self).__init__( 357 app, bus, self.verbs, self.rules, self.content_type) 358 self.service = '' 359 self.interface = '' 360 361 def find(self, path, method): 362 method_list = [] 363 buses = self.try_mapper_call( 364 self.mapper.get_object, path=path) 365 for items in buses.items(): 366 m = self.find_method_on_bus(path, method, *items) 367 if m: 368 method_list.append(m) 369 if method_list: 370 return method_list 371 372 abort(404, _4034_msg % ('method', 'found', method)) 373 374 def setup(self, path, method): 375 request.route_data['map'] = self.find(path, method) 376 377 def do_post(self, path, method, retry=True): 378 try: 379 args = [] 380 if request.parameter_list: 381 args = request.parameter_list 382 # To see if the return type is capable of being merged 383 if len(request.route_data['map']) > 1: 384 results = None 385 for item in request.route_data['map']: 386 tmp = item(*args) 387 if not results: 388 if tmp is not None: 389 results = type(tmp)() 390 if isinstance(results, dict): 391 results = results.update(tmp) 392 elif isinstance(results, list): 393 results = results + tmp 394 elif isinstance(results, type(None)): 395 results = None 396 else: 397 abort(501, 'Don\'t know how to merge method call ' 398 'results of {}'.format(type(tmp))) 399 return results 400 # There is only one method 401 return request.route_data['map'][0](*args) 402 403 except dbus.exceptions.DBusException as e: 404 paramlist = [] 405 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry: 406 407 signature_list = get_method_signature(self.bus, self.service, 408 path, self.interface, 409 method) 410 if not signature_list: 411 abort(400, "Failed to get method signature: %s" % str(e)) 412 if len(signature_list) != len(request.parameter_list): 413 abort(400, "Invalid number of args") 414 converted_value = None 415 try: 416 for index, expected_type in enumerate(signature_list): 417 value = request.parameter_list[index] 418 converted_value = convert_type(expected_type, value) 419 paramlist.append(converted_value) 420 request.parameter_list = paramlist 421 self.do_post(path, method, False) 422 return 423 except Exception as ex: 424 abort(400, "Bad Request/Invalid Args given") 425 abort(400, str(e)) 426 427 if e.get_dbus_name() == DBUS_TYPE_ERROR: 428 abort(400, str(e)) 429 raise 430 431 @staticmethod 432 def find_method_in_interface(method, obj, interface, methods): 433 if methods is None: 434 return None 435 436 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys())) 437 if method is not None: 438 iface = dbus.Interface(obj, interface) 439 return iface.get_dbus_method(method) 440 441 def find_method_on_bus(self, path, method, bus, interfaces): 442 obj = self.bus.get_object(bus, path, introspect=False) 443 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 444 data = iface.Introspect() 445 parser = IntrospectionNodeParser( 446 ElementTree.fromstring(data), 447 intf_match=lambda x: x in interfaces) 448 for x, y in parser.get_interfaces().items(): 449 m = self.find_method_in_interface( 450 method, obj, x, y.get('method')) 451 if m: 452 self.service = bus 453 self.interface = x 454 return m 455 456 457class PropertyHandler(RouteHandler): 458 verbs = ['PUT', 'GET'] 459 rules = '<path:path>/attr/<prop>' 460 content_type = 'application/json' 461 462 def __init__(self, app, bus): 463 super(PropertyHandler, self).__init__( 464 app, bus, self.verbs, self.rules, self.content_type) 465 466 def find(self, path, prop): 467 self.app.instance_handler.setup(path) 468 obj = self.app.instance_handler.do_get(path) 469 real_name = obmc.utils.misc.find_case_insensitive( 470 prop, list(obj.keys())) 471 472 if not real_name: 473 if request.method == 'PUT': 474 abort(403, _4034_msg % ('property', 'created', prop)) 475 else: 476 abort(404, _4034_msg % ('property', 'found', prop)) 477 return real_name, {path: obj} 478 479 def setup(self, path, prop): 480 name, obj = self.find(path, prop) 481 request.route_data['obj'] = obj 482 request.route_data['name'] = name 483 484 def do_get(self, path, prop): 485 name = request.route_data['name'] 486 return request.route_data['obj'][path][name] 487 488 def do_put(self, path, prop, value=None, retry=True): 489 if value is None: 490 value = request.parameter_list 491 492 prop, iface, properties_iface = self.get_host_interface( 493 path, prop, request.route_data['map'][path]) 494 try: 495 properties_iface.Set(iface, prop, value) 496 except ValueError as e: 497 abort(400, str(e)) 498 except dbus.exceptions.DBusException as e: 499 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry: 500 bus_name = properties_iface.bus_name 501 expected_type = get_type_signature_by_introspection(self.bus, 502 bus_name, 503 path, 504 prop) 505 if not expected_type: 506 abort(403, "Failed to get expected type: %s" % str(e)) 507 converted_value = None 508 try: 509 converted_value = convert_type(expected_type, value) 510 self.do_put(path, prop, converted_value, False) 511 return 512 except Exception as ex: 513 abort(403, "Failed to convert %s to type %s" % 514 (value, expected_type)) 515 abort(403, str(e)) 516 raise 517 518 def get_host_interface(self, path, prop, bus_info): 519 for bus, interfaces in bus_info.items(): 520 obj = self.bus.get_object(bus, path, introspect=True) 521 properties_iface = dbus.Interface( 522 obj, dbus_interface=dbus.PROPERTIES_IFACE) 523 524 info = self.get_host_interface_on_bus( 525 path, prop, properties_iface, bus, interfaces) 526 if info is not None: 527 prop, iface = info 528 return prop, iface, properties_iface 529 530 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces): 531 for i in interfaces: 532 properties = self.try_properties_interface(iface.GetAll, i) 533 if not properties: 534 continue 535 match = obmc.utils.misc.find_case_insensitive( 536 prop, list(properties.keys())) 537 if match is None: 538 continue 539 prop = match 540 return prop, i 541 542 543class SchemaHandler(RouteHandler): 544 verbs = ['GET'] 545 rules = '<path:path>/schema' 546 547 def __init__(self, app, bus): 548 super(SchemaHandler, self).__init__( 549 app, bus, self.verbs, self.rules) 550 551 def find(self, path): 552 return self.try_mapper_call( 553 self.mapper.get_object, 554 path=path) 555 556 def setup(self, path): 557 request.route_data['map'] = self.find(path) 558 559 def do_get(self, path): 560 schema = {} 561 for x in request.route_data['map'].keys(): 562 obj = self.bus.get_object(x, path, introspect=False) 563 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 564 data = iface.Introspect() 565 parser = IntrospectionNodeParser( 566 ElementTree.fromstring(data)) 567 for x, y in parser.get_interfaces().items(): 568 schema[x] = y 569 570 return schema 571 572 573class InstanceHandler(RouteHandler): 574 verbs = ['GET', 'PUT', 'DELETE'] 575 rules = '<path:path>' 576 request_type = dict 577 578 def __init__(self, app, bus): 579 super(InstanceHandler, self).__init__( 580 app, bus, self.verbs, self.rules) 581 582 def find(self, path, callback=None): 583 return {path: self.try_mapper_call( 584 self.mapper.get_object, 585 callback, 586 path=path)} 587 588 def setup(self, path): 589 callback = None 590 if request.method == 'PUT': 591 def callback(e, **kw): 592 abort(403, _4034_msg % ('resource', 'created', path)) 593 594 if request.route_data.get('map') is None: 595 request.route_data['map'] = self.find(path, callback) 596 597 def do_get(self, path): 598 return self.mapper.enumerate_object( 599 path, 600 mapper_data=request.route_data['map']) 601 602 def do_put(self, path): 603 # make sure all properties exist in the request 604 obj = set(self.do_get(path).keys()) 605 req = set(request.parameter_list.keys()) 606 607 diff = list(obj.difference(req)) 608 if diff: 609 abort(403, _4034_msg % ( 610 'resource', 'removed', '%s/attr/%s' % (path, diff[0]))) 611 612 diff = list(req.difference(obj)) 613 if diff: 614 abort(403, _4034_msg % ( 615 'resource', 'created', '%s/attr/%s' % (path, diff[0]))) 616 617 for p, v in request.parameter_list.items(): 618 self.app.property_handler.do_put( 619 path, p, v) 620 621 def do_delete(self, path): 622 deleted = False 623 for bus, interfaces in request.route_data['map'][path].items(): 624 if self.bus_has_delete(interfaces): 625 self.delete_on_bus(path, bus) 626 deleted = True 627 628 #It's OK if some objects didn't have a Delete, but not all 629 if not deleted: 630 abort(403, _4034_msg % ('resource', 'removed', path)) 631 632 def bus_has_delete(self, interfaces): 633 return DELETE_IFACE in interfaces 634 635 def delete_on_bus(self, path, bus): 636 obj = self.bus.get_object(bus, path, introspect=False) 637 delete_iface = dbus.Interface( 638 obj, dbus_interface=DELETE_IFACE) 639 delete_iface.Delete() 640 641 642class SessionHandler(MethodHandler): 643 ''' Handles the /login and /logout routes, manages 644 server side session store and session cookies. ''' 645 646 rules = ['/login', '/logout'] 647 login_str = "User '%s' logged %s" 648 bad_passwd_str = "Invalid username or password" 649 no_user_str = "No user logged in" 650 bad_json_str = "Expecting request format { 'data': " \ 651 "[<username>, <password>] }, got '%s'" 652 bmc_not_ready_str = "BMC is not ready (booting)" 653 _require_auth = None 654 MAX_SESSIONS = 16 655 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC' 656 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0' 657 BMCSTATE_PROPERTY = 'CurrentBMCState' 658 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready' 659 660 def __init__(self, app, bus): 661 super(SessionHandler, self).__init__( 662 app, bus) 663 self.hmac_key = os.urandom(128) 664 self.session_store = [] 665 666 @staticmethod 667 def authenticate(username, clear): 668 try: 669 encoded = spwd.getspnam(username)[1] 670 return encoded == crypt.crypt(clear, encoded) 671 except KeyError: 672 return False 673 674 def invalidate_session(self, session): 675 try: 676 self.session_store.remove(session) 677 except ValueError: 678 pass 679 680 def new_session(self): 681 sid = os.urandom(32) 682 if self.MAX_SESSIONS <= len(self.session_store): 683 self.session_store.pop() 684 self.session_store.insert(0, {'sid': sid}) 685 686 return self.session_store[0] 687 688 def get_session(self, sid): 689 sids = [x['sid'] for x in self.session_store] 690 try: 691 return self.session_store[sids.index(sid)] 692 except ValueError: 693 return None 694 695 def get_session_from_cookie(self): 696 return self.get_session( 697 request.get_cookie( 698 'sid', secret=self.hmac_key)) 699 700 def do_post(self, **kw): 701 if request.path == '/login': 702 return self.do_login(**kw) 703 else: 704 return self.do_logout(**kw) 705 706 def do_logout(self, **kw): 707 session = self.get_session_from_cookie() 708 if session is not None: 709 user = session['user'] 710 self.invalidate_session(session) 711 response.delete_cookie('sid') 712 return self.login_str % (user, 'out') 713 714 return self.no_user_str 715 716 def do_login(self, **kw): 717 if len(request.parameter_list) != 2: 718 abort(400, self.bad_json_str % (request.json)) 719 720 if not self.authenticate(*request.parameter_list): 721 abort(401, self.bad_passwd_str) 722 723 force = False 724 try: 725 force = request.json.get('force') 726 except (ValueError, AttributeError, KeyError, TypeError): 727 force = False 728 729 if not force and not self.is_bmc_ready(): 730 abort(503, self.bmc_not_ready_str) 731 732 user = request.parameter_list[0] 733 session = self.new_session() 734 session['user'] = user 735 response.set_cookie( 736 'sid', session['sid'], secret=self.hmac_key, 737 secure=True, 738 httponly=True) 739 return self.login_str % (user, 'in') 740 741 def is_bmc_ready(self): 742 if not self.app.with_bmc_check: 743 return True 744 745 try: 746 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH) 747 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE) 748 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY) 749 if state == self.BMCSTATE_READY: 750 return True 751 752 except dbus.exceptions.DBusException: 753 pass 754 755 return False 756 757 def find(self, **kw): 758 pass 759 760 def setup(self, **kw): 761 pass 762 763 764class ImageUploadUtils: 765 ''' Provides common utils for image upload. ''' 766 767 file_loc = '/tmp/images' 768 file_prefix = 'img' 769 file_suffix = '' 770 signal = None 771 772 @classmethod 773 def do_upload(cls, filename=''): 774 def cleanup(): 775 os.close(handle) 776 if cls.signal: 777 cls.signal.remove() 778 cls.signal = None 779 780 def signal_callback(path, a, **kw): 781 # Just interested on the first Version interface created which is 782 # triggered when the file is uploaded. This helps avoid getting the 783 # wrong information for multiple upload requests in a row. 784 if "xyz.openbmc_project.Software.Version" in a and \ 785 "xyz.openbmc_project.Software.Activation" not in a: 786 paths.append(path) 787 788 while cls.signal: 789 # Serialize uploads by waiting for the signal to be cleared. 790 # This makes it easier to ensure that the version information 791 # is the right one instead of the data from another upload request. 792 gevent.sleep(1) 793 if not os.path.exists(cls.file_loc): 794 abort(500, "Error Directory not found") 795 paths = [] 796 bus = dbus.SystemBus() 797 cls.signal = bus.add_signal_receiver( 798 signal_callback, 799 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 800 signal_name='InterfacesAdded', 801 path=SOFTWARE_PATH) 802 if not filename: 803 handle, filename = tempfile.mkstemp(cls.file_suffix, 804 cls.file_prefix, cls.file_loc) 805 else: 806 filename = os.path.join(cls.file_loc, filename) 807 handle = os.open(filename, os.O_WRONLY | os.O_CREAT) 808 try: 809 file_contents = request.body.read() 810 request.body.close() 811 os.write(handle, file_contents) 812 # Close file after writing, the image manager process watches for 813 # the close event to know the upload is complete. 814 os.close(handle) 815 except (IOError, ValueError) as e: 816 cleanup() 817 abort(400, str(e)) 818 except Exception: 819 cleanup() 820 abort(400, "Unexpected Error") 821 loop = gobject.MainLoop() 822 gcontext = loop.get_context() 823 count = 0 824 version_id = '' 825 while loop is not None: 826 try: 827 if gcontext.pending(): 828 gcontext.iteration() 829 if not paths: 830 gevent.sleep(1) 831 else: 832 version_id = os.path.basename(paths.pop()) 833 break 834 count += 1 835 if count == 10: 836 break 837 except Exception: 838 break 839 cls.signal.remove() 840 cls.signal = None 841 if version_id: 842 return version_id 843 else: 844 abort(400, "Version already exists or failed to be extracted") 845 846 847class ImagePostHandler(RouteHandler): 848 ''' Handles the /upload/image route. ''' 849 850 verbs = ['POST'] 851 rules = ['/upload/image'] 852 content_type = 'application/octet-stream' 853 854 def __init__(self, app, bus): 855 super(ImagePostHandler, self).__init__( 856 app, bus, self.verbs, self.rules, self.content_type) 857 858 def do_post(self, filename=''): 859 return ImageUploadUtils.do_upload() 860 861 def find(self, **kw): 862 pass 863 864 def setup(self, **kw): 865 pass 866 867 868class EventNotifier: 869 keyNames = {} 870 keyNames['event'] = 'event' 871 keyNames['path'] = 'path' 872 keyNames['intfMap'] = 'interfaces' 873 keyNames['propMap'] = 'properties' 874 keyNames['intf'] = 'interface' 875 876 def __init__(self, wsock, filters): 877 self.wsock = wsock 878 self.paths = filters.get("paths", []) 879 self.interfaces = filters.get("interfaces", []) 880 if not self.paths: 881 self.paths.append(None) 882 bus = dbus.SystemBus() 883 # Add a signal receiver for every path the client is interested in 884 for path in self.paths: 885 bus.add_signal_receiver( 886 self.interfaces_added_handler, 887 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 888 signal_name='InterfacesAdded', 889 path=path) 890 bus.add_signal_receiver( 891 self.properties_changed_handler, 892 dbus_interface=dbus.PROPERTIES_IFACE, 893 signal_name='PropertiesChanged', 894 path=path, 895 path_keyword='path') 896 loop = gobject.MainLoop() 897 # gobject's mainloop.run() will block the entire process, so the gevent 898 # scheduler and hence greenlets won't execute. The while-loop below 899 # works around this limitation by using gevent's sleep, instead of 900 # calling loop.run() 901 gcontext = loop.get_context() 902 while loop is not None: 903 try: 904 if gcontext.pending(): 905 gcontext.iteration() 906 else: 907 # gevent.sleep puts only the current greenlet to sleep, 908 # not the entire process. 909 gevent.sleep(5) 910 except WebSocketError: 911 break 912 913 def interfaces_added_handler(self, path, iprops, **kw): 914 ''' If the client is interested in these changes, respond to the 915 client. This handles d-bus interface additions.''' 916 if (not self.interfaces) or \ 917 (not set(iprops).isdisjoint(self.interfaces)): 918 response = {} 919 response[self.keyNames['event']] = "InterfacesAdded" 920 response[self.keyNames['path']] = path 921 response[self.keyNames['intfMap']] = iprops 922 try: 923 self.wsock.send(json.dumps(response)) 924 except WebSocketError: 925 return 926 927 def properties_changed_handler(self, interface, new, old, **kw): 928 ''' If the client is interested in these changes, respond to the 929 client. This handles d-bus property changes. ''' 930 if (not self.interfaces) or (interface in self.interfaces): 931 path = str(kw['path']) 932 response = {} 933 response[self.keyNames['event']] = "PropertiesChanged" 934 response[self.keyNames['path']] = path 935 response[self.keyNames['intf']] = interface 936 response[self.keyNames['propMap']] = new 937 try: 938 self.wsock.send(json.dumps(response)) 939 except WebSocketError: 940 return 941 942 943class EventHandler(RouteHandler): 944 ''' Handles the /subscribe route, for clients to be able 945 to subscribe to BMC events. ''' 946 947 verbs = ['GET'] 948 rules = ['/subscribe'] 949 950 def __init__(self, app, bus): 951 super(EventHandler, self).__init__( 952 app, bus, self.verbs, self.rules) 953 954 def find(self, **kw): 955 pass 956 957 def setup(self, **kw): 958 pass 959 960 def do_get(self): 961 wsock = request.environ.get('wsgi.websocket') 962 if not wsock: 963 abort(400, 'Expected WebSocket request.') 964 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 965 filters = wsock.receive() 966 filters = json.loads(filters) 967 notifier = EventNotifier(wsock, filters) 968 969class HostConsoleHandler(RouteHandler): 970 ''' Handles the /console route, for clients to be able 971 read/write the host serial console. The way this is 972 done is by exposing a websocket that's mirrored to an 973 abstract UNIX domain socket, which is the source for 974 the console data. ''' 975 976 verbs = ['GET'] 977 # Naming the route console0, because the numbering will help 978 # on multi-bmc/multi-host systems. 979 rules = ['/console0'] 980 981 def __init__(self, app, bus): 982 super(HostConsoleHandler, self).__init__( 983 app, bus, self.verbs, self.rules) 984 985 def find(self, **kw): 986 pass 987 988 def setup(self, **kw): 989 pass 990 991 def read_wsock(self, wsock, sock): 992 while True: 993 try: 994 incoming = wsock.receive() 995 if incoming: 996 # Read websocket, write to UNIX socket 997 sock.send(incoming) 998 except Exception as e: 999 sock.close() 1000 return 1001 1002 def read_sock(self, sock, wsock): 1003 max_sock_read_len = 4096 1004 while True: 1005 try: 1006 outgoing = sock.recv(max_sock_read_len) 1007 if outgoing: 1008 # Read UNIX socket, write to websocket 1009 wsock.send(outgoing) 1010 except Exception as e: 1011 wsock.close() 1012 return 1013 1014 def do_get(self): 1015 wsock = request.environ.get('wsgi.websocket') 1016 if not wsock: 1017 abort(400, 'Expected WebSocket based request.') 1018 1019 # A UNIX domain socket structure defines a 108-byte pathname. The 1020 # server in this case, obmc-console-server, expects a 108-byte path. 1021 socket_name = "\0obmc-console" 1022 trailing_bytes = "\0" * (108 - len(socket_name)) 1023 socket_path = socket_name + trailing_bytes 1024 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1025 1026 try: 1027 sock.connect(socket_path) 1028 except Exception as e: 1029 abort(500, str(e)) 1030 1031 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock) 1032 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock) 1033 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 1034 gevent.joinall([wsock_reader, sock_reader, ping_sender]) 1035 1036 1037class ImagePutHandler(RouteHandler): 1038 ''' Handles the /upload/image/<filename> route. ''' 1039 1040 verbs = ['PUT'] 1041 rules = ['/upload/image/<filename>'] 1042 content_type = 'application/octet-stream' 1043 1044 def __init__(self, app, bus): 1045 super(ImagePutHandler, self).__init__( 1046 app, bus, self.verbs, self.rules, self.content_type) 1047 1048 def do_put(self, filename=''): 1049 return ImageUploadUtils.do_upload(filename) 1050 1051 def find(self, **kw): 1052 pass 1053 1054 def setup(self, **kw): 1055 pass 1056 1057 1058class DownloadDumpHandler(RouteHandler): 1059 ''' Handles the /download/dump route. ''' 1060 1061 verbs = 'GET' 1062 rules = ['/download/dump/<dumpid>'] 1063 content_type = 'application/octet-stream' 1064 dump_loc = '/var/lib/phosphor-debug-collector/dumps' 1065 suppress_json_resp = True 1066 1067 def __init__(self, app, bus): 1068 super(DownloadDumpHandler, self).__init__( 1069 app, bus, self.verbs, self.rules, self.content_type) 1070 1071 def do_get(self, dumpid): 1072 return self.do_download(dumpid) 1073 1074 def find(self, **kw): 1075 pass 1076 1077 def setup(self, **kw): 1078 pass 1079 1080 def do_download(self, dumpid): 1081 dump_loc = os.path.join(self.dump_loc, dumpid) 1082 if not os.path.exists(dump_loc): 1083 abort(404, "Path not found") 1084 1085 files = os.listdir(dump_loc) 1086 num_files = len(files) 1087 if num_files == 0: 1088 abort(404, "Dump not found") 1089 1090 return static_file(os.path.basename(files[0]), root=dump_loc, 1091 download=True, mimetype=self.content_type) 1092 1093 1094class WebHandler(RouteHandler): 1095 ''' Handles the routes for the web UI files. ''' 1096 1097 verbs = 'GET' 1098 1099 # Match only what we know are web files, so everything else 1100 # can get routed to the REST handlers. 1101 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>', 1102 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>', 1103 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>', 1104 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>', 1105 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>', 1106 '/<filename:re:.+\.ico>'] 1107 1108 # The mimetypes module knows about most types, but not these 1109 content_types = { 1110 '.eot': 'application/vnd.ms-fontobject', 1111 '.woff': 'application/x-font-woff', 1112 '.woff2': 'application/x-font-woff2', 1113 '.ttf': 'application/x-font-ttf', 1114 '.map': 'application/json' 1115 } 1116 1117 _require_auth = None 1118 suppress_json_resp = True 1119 1120 def __init__(self, app, bus): 1121 super(WebHandler, self).__init__( 1122 app, bus, self.verbs, self.rules) 1123 1124 def get_type(self, filename): 1125 ''' Returns the content type and encoding for a file ''' 1126 1127 content_type, encoding = mimetypes.guess_type(filename) 1128 1129 # Try our own list if mimetypes didn't recognize it 1130 if content_type is None: 1131 if filename[-3:] == '.gz': 1132 filename = filename[:-3] 1133 extension = filename[filename.rfind('.'):] 1134 content_type = self.content_types.get(extension, None) 1135 1136 return content_type, encoding 1137 1138 def do_get(self, filename='index.html'): 1139 1140 # If a gzipped version exists, use that instead. 1141 # Possible future enhancement: if the client doesn't 1142 # accept compressed files, unzip it ourselves before sending. 1143 if not os.path.exists(os.path.join(www_base_path, filename)): 1144 filename = filename + '.gz' 1145 1146 # Though bottle should protect us, ensure path is valid 1147 realpath = os.path.realpath(filename) 1148 if realpath[0] == '/': 1149 realpath = realpath[1:] 1150 if not os.path.exists(os.path.join(www_base_path, realpath)): 1151 abort(404, "Path not found") 1152 1153 mimetype, encoding = self.get_type(filename) 1154 1155 # Couldn't find the type - let static_file() deal with it, 1156 # though this should never happen. 1157 if mimetype is None: 1158 print("Can't figure out content-type for %s" % filename) 1159 mimetype = 'auto' 1160 1161 # This call will set several header fields for us, 1162 # including the charset if the type is text. 1163 response = static_file(filename, www_base_path, mimetype) 1164 1165 # static_file() will only set the encoding if the 1166 # mimetype was auto, so set it here. 1167 if encoding is not None: 1168 response.set_header('Content-Encoding', encoding) 1169 1170 return response 1171 1172 def find(self, **kw): 1173 pass 1174 1175 def setup(self, **kw): 1176 pass 1177 1178 1179class AuthorizationPlugin(object): 1180 ''' Invokes an optional list of authorization callbacks. ''' 1181 1182 name = 'authorization' 1183 api = 2 1184 1185 class Compose: 1186 def __init__(self, validators, callback, session_mgr): 1187 self.validators = validators 1188 self.callback = callback 1189 self.session_mgr = session_mgr 1190 1191 def __call__(self, *a, **kw): 1192 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) 1193 session = self.session_mgr.get_session(sid) 1194 if request.method != 'OPTIONS': 1195 for x in self.validators: 1196 x(session, *a, **kw) 1197 1198 return self.callback(*a, **kw) 1199 1200 def apply(self, callback, route): 1201 undecorated = route.get_undecorated_callback() 1202 if not isinstance(undecorated, RouteHandler): 1203 return callback 1204 1205 auth_types = getattr( 1206 undecorated, '_require_auth', None) 1207 if not auth_types: 1208 return callback 1209 1210 return self.Compose( 1211 auth_types, callback, undecorated.app.session_handler) 1212 1213 1214class CorsPlugin(object): 1215 ''' Add CORS headers. ''' 1216 1217 name = 'cors' 1218 api = 2 1219 1220 @staticmethod 1221 def process_origin(): 1222 origin = request.headers.get('Origin') 1223 if origin: 1224 response.add_header('Access-Control-Allow-Origin', origin) 1225 response.add_header( 1226 'Access-Control-Allow-Credentials', 'true') 1227 1228 @staticmethod 1229 def process_method_and_headers(verbs): 1230 method = request.headers.get('Access-Control-Request-Method') 1231 headers = request.headers.get('Access-Control-Request-Headers') 1232 if headers: 1233 headers = [x.lower() for x in headers.split(',')] 1234 1235 if method in verbs \ 1236 and headers == ['content-type']: 1237 response.add_header('Access-Control-Allow-Methods', method) 1238 response.add_header( 1239 'Access-Control-Allow-Headers', 'Content-Type') 1240 response.add_header('X-Frame-Options', 'deny') 1241 response.add_header('X-Content-Type-Options', 'nosniff') 1242 response.add_header('X-XSS-Protection', '1; mode=block') 1243 response.add_header( 1244 'Content-Security-Policy', "default-src 'self'") 1245 response.add_header( 1246 'Strict-Transport-Security', 1247 'max-age=31536000; includeSubDomains; preload') 1248 1249 def __init__(self, app): 1250 app.install_error_callback(self.error_callback) 1251 1252 def apply(self, callback, route): 1253 undecorated = route.get_undecorated_callback() 1254 if not isinstance(undecorated, RouteHandler): 1255 return callback 1256 1257 if not getattr(undecorated, '_enable_cors', None): 1258 return callback 1259 1260 def wrap(*a, **kw): 1261 self.process_origin() 1262 self.process_method_and_headers(undecorated._verbs) 1263 return callback(*a, **kw) 1264 1265 return wrap 1266 1267 def error_callback(self, **kw): 1268 self.process_origin() 1269 1270 1271class JsonApiRequestPlugin(object): 1272 ''' Ensures request content satisfies the OpenBMC json api format. ''' 1273 name = 'json_api_request' 1274 api = 2 1275 1276 error_str = "Expecting request format { 'data': <value> }, got '%s'" 1277 type_error_str = "Unsupported Content-Type: '%s'" 1278 json_type = "application/json" 1279 request_methods = ['PUT', 'POST', 'PATCH'] 1280 1281 @staticmethod 1282 def content_expected(): 1283 return request.method in JsonApiRequestPlugin.request_methods 1284 1285 def validate_request(self): 1286 if request.content_length > 0 and \ 1287 request.content_type != self.json_type: 1288 abort(415, self.type_error_str % request.content_type) 1289 1290 try: 1291 request.parameter_list = request.json.get('data') 1292 except ValueError as e: 1293 abort(400, str(e)) 1294 except (AttributeError, KeyError, TypeError): 1295 abort(400, self.error_str % request.json) 1296 1297 def apply(self, callback, route): 1298 content_type = getattr( 1299 route.get_undecorated_callback(), '_content_type', None) 1300 if self.json_type != content_type: 1301 return callback 1302 1303 verbs = getattr( 1304 route.get_undecorated_callback(), '_verbs', None) 1305 if verbs is None: 1306 return callback 1307 1308 if not set(self.request_methods).intersection(verbs): 1309 return callback 1310 1311 def wrap(*a, **kw): 1312 if self.content_expected(): 1313 self.validate_request() 1314 return callback(*a, **kw) 1315 1316 return wrap 1317 1318 1319class JsonApiRequestTypePlugin(object): 1320 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1321 name = 'json_api_method_request' 1322 api = 2 1323 1324 error_str = "Expecting request format { 'data': %s }, got '%s'" 1325 json_type = "application/json" 1326 1327 def apply(self, callback, route): 1328 content_type = getattr( 1329 route.get_undecorated_callback(), '_content_type', None) 1330 if self.json_type != content_type: 1331 return callback 1332 1333 request_type = getattr( 1334 route.get_undecorated_callback(), 'request_type', None) 1335 if request_type is None: 1336 return callback 1337 1338 def validate_request(): 1339 if not isinstance(request.parameter_list, request_type): 1340 abort(400, self.error_str % (str(request_type), request.json)) 1341 1342 def wrap(*a, **kw): 1343 if JsonApiRequestPlugin.content_expected(): 1344 validate_request() 1345 return callback(*a, **kw) 1346 1347 return wrap 1348 1349 1350class JsonErrorsPlugin(JSONPlugin): 1351 ''' Extend the Bottle JSONPlugin such that it also encodes error 1352 responses. ''' 1353 1354 def __init__(self, app, **kw): 1355 super(JsonErrorsPlugin, self).__init__(**kw) 1356 self.json_opts = { 1357 x: y for x, y in kw.items() 1358 if x in ['indent', 'sort_keys']} 1359 app.install_error_callback(self.error_callback) 1360 1361 def error_callback(self, response_object, response_body, **kw): 1362 response_body['body'] = json.dumps(response_object, **self.json_opts) 1363 response.content_type = 'application/json' 1364 1365 1366class JsonApiResponsePlugin(object): 1367 ''' Emits responses in the OpenBMC json api format. ''' 1368 name = 'json_api_response' 1369 api = 2 1370 1371 @staticmethod 1372 def has_body(): 1373 return request.method not in ['OPTIONS'] 1374 1375 def __init__(self, app): 1376 app.install_error_callback(self.error_callback) 1377 1378 def apply(self, callback, route): 1379 skip = getattr( 1380 route.get_undecorated_callback(), 'suppress_json_resp', None) 1381 if skip: 1382 return callback 1383 1384 def wrap(*a, **kw): 1385 data = callback(*a, **kw) 1386 if self.has_body(): 1387 resp = {'data': data} 1388 resp['status'] = 'ok' 1389 resp['message'] = response.status_line 1390 return resp 1391 return wrap 1392 1393 def error_callback(self, error, response_object, **kw): 1394 response_object['message'] = error.status_line 1395 response_object['status'] = 'error' 1396 response_object.setdefault('data', {})['description'] = str(error.body) 1397 if error.status_code == 500: 1398 response_object['data']['exception'] = repr(error.exception) 1399 response_object['data']['traceback'] = error.traceback.splitlines() 1400 1401 1402class JsonpPlugin(object): 1403 ''' Json javascript wrapper. ''' 1404 name = 'jsonp' 1405 api = 2 1406 1407 def __init__(self, app, **kw): 1408 app.install_error_callback(self.error_callback) 1409 1410 @staticmethod 1411 def to_jsonp(json): 1412 jwrapper = request.query.callback or None 1413 if(jwrapper): 1414 response.set_header('Content-Type', 'application/javascript') 1415 json = jwrapper + '(' + json + ');' 1416 return json 1417 1418 def apply(self, callback, route): 1419 def wrap(*a, **kw): 1420 return self.to_jsonp(callback(*a, **kw)) 1421 return wrap 1422 1423 def error_callback(self, response_body, **kw): 1424 response_body['body'] = self.to_jsonp(response_body['body']) 1425 1426 1427class ContentCheckerPlugin(object): 1428 ''' Ensures that a route is associated with the expected content-type 1429 header. ''' 1430 name = 'content_checker' 1431 api = 2 1432 1433 class Checker: 1434 def __init__(self, type, callback): 1435 self.expected_type = type 1436 self.callback = callback 1437 self.error_str = "Expecting content type '%s', got '%s'" 1438 1439 def __call__(self, *a, **kw): 1440 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1441 self.expected_type and \ 1442 self.expected_type != request.content_type: 1443 abort(415, self.error_str % (self.expected_type, 1444 request.content_type)) 1445 1446 return self.callback(*a, **kw) 1447 1448 def apply(self, callback, route): 1449 content_type = getattr( 1450 route.get_undecorated_callback(), '_content_type', None) 1451 1452 return self.Checker(content_type, callback) 1453 1454class CheckURLPlugin(object): 1455 ''' Ensures that anything read and written using only urls listed in 1456 the url_config.json config file would allowed. ''' 1457 name = 'url_checker' 1458 api = 2 1459 1460 def __init__(self): 1461 config_path = '/usr/share/rest-dbus/url_config.json' 1462 url_config = {} 1463 urls = {} 1464 self.pattern = {} 1465 if os.path.exists(config_path): 1466 try: 1467 with open(config_path) as data_file: 1468 url_config = json.load(data_file) 1469 urls = url_config.get("urls", ["*"]) 1470 self.pattern = '|'.join(fnmatch.translate(p) for p in urls) 1471 self.pattern = re.compile(self.pattern) 1472 except ValueError as e: 1473 abort(404, str(e)) 1474 else: 1475 abort(404, "Config file path not found for Whitelisted URLs") 1476 1477 def apply(self, callback, route): 1478 1479 def wrap(*a, **kw): 1480 if self.pattern.match(request.path): 1481 return callback(*a, **kw) 1482 abort(404,"Trying to access Blocked URL") 1483 return wrap 1484 1485class App(Bottle): 1486 def __init__(self, **kw): 1487 super(App, self).__init__(autojson=False) 1488 1489 self.have_wsock = kw.get('have_wsock', False) 1490 self.with_bmc_check = '--with-bmc-check' in sys.argv 1491 1492 self.bus = dbus.SystemBus() 1493 self.mapper = obmc.mapper.Mapper(self.bus) 1494 self.error_callbacks = [] 1495 1496 self.install_hooks() 1497 self.install_plugins() 1498 self.create_handlers() 1499 self.install_handlers() 1500 1501 def install_plugins(self): 1502 # install json api plugins 1503 json_kw = {'indent': 2, 'sort_keys': True} 1504 self.install(AuthorizationPlugin()) 1505 self.install(CorsPlugin(self)) 1506 self.install(ContentCheckerPlugin()) 1507 self.install(JsonpPlugin(self, **json_kw)) 1508 self.install(JsonErrorsPlugin(self, **json_kw)) 1509 self.install(JsonApiResponsePlugin(self)) 1510 self.install(JsonApiRequestPlugin()) 1511 self.install(JsonApiRequestTypePlugin()) 1512 self.install(CheckURLPlugin()) 1513 1514 def install_hooks(self): 1515 self.error_handler_type = type(self.default_error_handler) 1516 self.original_error_handler = self.default_error_handler 1517 self.default_error_handler = self.error_handler_type( 1518 self.custom_error_handler, self, Bottle) 1519 1520 self.real_router_match = self.router.match 1521 self.router.match = self.custom_router_match 1522 self.add_hook('before_request', self.strip_extra_slashes) 1523 1524 def create_handlers(self): 1525 # create route handlers 1526 self.session_handler = SessionHandler(self, self.bus) 1527 self.web_handler = WebHandler(self, self.bus) 1528 self.directory_handler = DirectoryHandler(self, self.bus) 1529 self.list_names_handler = ListNamesHandler(self, self.bus) 1530 self.list_handler = ListHandler(self, self.bus) 1531 self.method_handler = MethodHandler(self, self.bus) 1532 self.property_handler = PropertyHandler(self, self.bus) 1533 self.schema_handler = SchemaHandler(self, self.bus) 1534 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1535 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1536 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1537 if self.have_wsock: 1538 self.event_handler = EventHandler(self, self.bus) 1539 self.host_console_handler = HostConsoleHandler(self, self.bus) 1540 self.instance_handler = InstanceHandler(self, self.bus) 1541 1542 def install_handlers(self): 1543 self.session_handler.install() 1544 self.web_handler.install() 1545 self.directory_handler.install() 1546 self.list_names_handler.install() 1547 self.list_handler.install() 1548 self.method_handler.install() 1549 self.property_handler.install() 1550 self.schema_handler.install() 1551 self.image_upload_post_handler.install() 1552 self.image_upload_put_handler.install() 1553 self.download_dump_get_handler.install() 1554 if self.have_wsock: 1555 self.event_handler.install() 1556 self.host_console_handler.install() 1557 # this has to come last, since it matches everything 1558 self.instance_handler.install() 1559 1560 def install_error_callback(self, callback): 1561 self.error_callbacks.insert(0, callback) 1562 1563 def custom_router_match(self, environ): 1564 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1565 needed doesn't work for us since the instance rules match 1566 everything. This monkey-patch lets the route handler figure 1567 out which response is needed. This could be accomplished 1568 with a hook but that would require calling the router match 1569 function twice. 1570 ''' 1571 route, args = self.real_router_match(environ) 1572 if isinstance(route.callback, RouteHandler): 1573 route.callback._setup(**args) 1574 1575 return route, args 1576 1577 def custom_error_handler(self, res, error): 1578 ''' Allow plugins to modify error responses too via this custom 1579 error handler. ''' 1580 1581 response_object = {} 1582 response_body = {} 1583 for x in self.error_callbacks: 1584 x(error=error, 1585 response_object=response_object, 1586 response_body=response_body) 1587 1588 return response_body.get('body', "") 1589 1590 @staticmethod 1591 def strip_extra_slashes(): 1592 path = request.environ['PATH_INFO'] 1593 trailing = ("", "/")[path[-1] == '/'] 1594 parts = list(filter(bool, path.split('/'))) 1595 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1596