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 34have_wsock = True 35try: 36 from geventwebsocket import WebSocketError 37except ImportError: 38 have_wsock = False 39if have_wsock: 40 from dbus.mainloop.glib import DBusGMainLoop 41 DBusGMainLoop(set_as_default=True) 42 # TODO: openbmc/openbmc#2994 remove python 2 support 43 try: # python 2 44 import gobject 45 except ImportError: # python 3 46 from gi.repository import GObject as gobject 47 import gevent 48 from gevent import socket 49 from gevent import Greenlet 50 51DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface' 52DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' 53DBUS_PROPERTY_READONLY = 'org.freedesktop.DBus.Error.PropertyReadOnly' 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_PROPERTY_READONLY: 500 abort(403, str(e)) 501 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry: 502 bus_name = properties_iface.bus_name 503 expected_type = get_type_signature_by_introspection(self.bus, 504 bus_name, 505 path, 506 prop) 507 if not expected_type: 508 abort(403, "Failed to get expected type: %s" % str(e)) 509 converted_value = None 510 try: 511 converted_value = convert_type(expected_type, value) 512 except Exception as ex: 513 abort(403, "Failed to convert %s to type %s" % 514 (value, expected_type)) 515 try: 516 self.do_put(path, prop, converted_value, False) 517 return 518 except Exception as ex: 519 abort(403, str(ex)) 520 521 abort(403, str(e)) 522 raise 523 524 def get_host_interface(self, path, prop, bus_info): 525 for bus, interfaces in bus_info.items(): 526 obj = self.bus.get_object(bus, path, introspect=True) 527 properties_iface = dbus.Interface( 528 obj, dbus_interface=dbus.PROPERTIES_IFACE) 529 530 info = self.get_host_interface_on_bus( 531 path, prop, properties_iface, bus, interfaces) 532 if info is not None: 533 prop, iface = info 534 return prop, iface, properties_iface 535 536 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces): 537 for i in interfaces: 538 properties = self.try_properties_interface(iface.GetAll, i) 539 if not properties: 540 continue 541 match = obmc.utils.misc.find_case_insensitive( 542 prop, list(properties.keys())) 543 if match is None: 544 continue 545 prop = match 546 return prop, i 547 548 549class SchemaHandler(RouteHandler): 550 verbs = ['GET'] 551 rules = '<path:path>/schema' 552 553 def __init__(self, app, bus): 554 super(SchemaHandler, self).__init__( 555 app, bus, self.verbs, self.rules) 556 557 def find(self, path): 558 return self.try_mapper_call( 559 self.mapper.get_object, 560 path=path) 561 562 def setup(self, path): 563 request.route_data['map'] = self.find(path) 564 565 def do_get(self, path): 566 schema = {} 567 for x in request.route_data['map'].keys(): 568 obj = self.bus.get_object(x, path, introspect=False) 569 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE) 570 data = iface.Introspect() 571 parser = IntrospectionNodeParser( 572 ElementTree.fromstring(data)) 573 for x, y in parser.get_interfaces().items(): 574 schema[x] = y 575 576 return schema 577 578 579class InstanceHandler(RouteHandler): 580 verbs = ['GET', 'PUT', 'DELETE'] 581 rules = '<path:path>' 582 request_type = dict 583 584 def __init__(self, app, bus): 585 super(InstanceHandler, self).__init__( 586 app, bus, self.verbs, self.rules) 587 588 def find(self, path, callback=None): 589 return {path: self.try_mapper_call( 590 self.mapper.get_object, 591 callback, 592 path=path)} 593 594 def setup(self, path): 595 callback = None 596 if request.method == 'PUT': 597 def callback(e, **kw): 598 abort(403, _4034_msg % ('resource', 'created', path)) 599 600 if request.route_data.get('map') is None: 601 request.route_data['map'] = self.find(path, callback) 602 603 def do_get(self, path): 604 return self.mapper.enumerate_object( 605 path, 606 mapper_data=request.route_data['map']) 607 608 def do_put(self, path): 609 # make sure all properties exist in the request 610 obj = set(self.do_get(path).keys()) 611 req = set(request.parameter_list.keys()) 612 613 diff = list(obj.difference(req)) 614 if diff: 615 abort(403, _4034_msg % ( 616 'resource', 'removed', '%s/attr/%s' % (path, diff[0]))) 617 618 diff = list(req.difference(obj)) 619 if diff: 620 abort(403, _4034_msg % ( 621 'resource', 'created', '%s/attr/%s' % (path, diff[0]))) 622 623 for p, v in request.parameter_list.items(): 624 self.app.property_handler.do_put( 625 path, p, v) 626 627 def do_delete(self, path): 628 deleted = False 629 for bus, interfaces in request.route_data['map'][path].items(): 630 if self.bus_has_delete(interfaces): 631 self.delete_on_bus(path, bus) 632 deleted = True 633 634 #It's OK if some objects didn't have a Delete, but not all 635 if not deleted: 636 abort(403, _4034_msg % ('resource', 'removed', path)) 637 638 def bus_has_delete(self, interfaces): 639 return DELETE_IFACE in interfaces 640 641 def delete_on_bus(self, path, bus): 642 obj = self.bus.get_object(bus, path, introspect=False) 643 delete_iface = dbus.Interface( 644 obj, dbus_interface=DELETE_IFACE) 645 delete_iface.Delete() 646 647 648class SessionHandler(MethodHandler): 649 ''' Handles the /login and /logout routes, manages 650 server side session store and session cookies. ''' 651 652 rules = ['/login', '/logout'] 653 login_str = "User '%s' logged %s" 654 bad_passwd_str = "Invalid username or password" 655 no_user_str = "No user logged in" 656 bad_json_str = "Expecting request format { 'data': " \ 657 "[<username>, <password>] }, got '%s'" 658 bmc_not_ready_str = "BMC is not ready (booting)" 659 _require_auth = None 660 MAX_SESSIONS = 16 661 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC' 662 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0' 663 BMCSTATE_PROPERTY = 'CurrentBMCState' 664 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready' 665 666 def __init__(self, app, bus): 667 super(SessionHandler, self).__init__( 668 app, bus) 669 self.hmac_key = os.urandom(128) 670 self.session_store = [] 671 672 @staticmethod 673 def authenticate(username, clear): 674 try: 675 encoded = spwd.getspnam(username)[1] 676 return encoded == crypt.crypt(clear, encoded) 677 except KeyError: 678 return False 679 680 def invalidate_session(self, session): 681 try: 682 self.session_store.remove(session) 683 except ValueError: 684 pass 685 686 def new_session(self): 687 sid = os.urandom(32) 688 if self.MAX_SESSIONS <= len(self.session_store): 689 self.session_store.pop() 690 self.session_store.insert(0, {'sid': sid}) 691 692 return self.session_store[0] 693 694 def get_session(self, sid): 695 sids = [x['sid'] for x in self.session_store] 696 try: 697 return self.session_store[sids.index(sid)] 698 except ValueError: 699 return None 700 701 def get_session_from_cookie(self): 702 return self.get_session( 703 request.get_cookie( 704 'sid', secret=self.hmac_key)) 705 706 def do_post(self, **kw): 707 if request.path == '/login': 708 return self.do_login(**kw) 709 else: 710 return self.do_logout(**kw) 711 712 def do_logout(self, **kw): 713 session = self.get_session_from_cookie() 714 if session is not None: 715 user = session['user'] 716 self.invalidate_session(session) 717 response.delete_cookie('sid') 718 return self.login_str % (user, 'out') 719 720 return self.no_user_str 721 722 def do_login(self, **kw): 723 if len(request.parameter_list) != 2: 724 abort(400, self.bad_json_str % (request.json)) 725 726 if not self.authenticate(*request.parameter_list): 727 abort(401, self.bad_passwd_str) 728 729 force = False 730 try: 731 force = request.json.get('force') 732 except (ValueError, AttributeError, KeyError, TypeError): 733 force = False 734 735 if not force and not self.is_bmc_ready(): 736 abort(503, self.bmc_not_ready_str) 737 738 user = request.parameter_list[0] 739 session = self.new_session() 740 session['user'] = user 741 response.set_cookie( 742 'sid', session['sid'], secret=self.hmac_key, 743 secure=True, 744 httponly=True) 745 return self.login_str % (user, 'in') 746 747 def is_bmc_ready(self): 748 if not self.app.with_bmc_check: 749 return True 750 751 try: 752 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH) 753 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE) 754 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY) 755 if state == self.BMCSTATE_READY: 756 return True 757 758 except dbus.exceptions.DBusException: 759 pass 760 761 return False 762 763 def find(self, **kw): 764 pass 765 766 def setup(self, **kw): 767 pass 768 769 770class ImageUploadUtils: 771 ''' Provides common utils for image upload. ''' 772 773 file_loc = '/tmp/images' 774 file_prefix = 'img' 775 file_suffix = '' 776 signal = None 777 778 @classmethod 779 def do_upload(cls, filename=''): 780 def cleanup(): 781 os.close(handle) 782 if cls.signal: 783 cls.signal.remove() 784 cls.signal = None 785 786 def signal_callback(path, a, **kw): 787 # Just interested on the first Version interface created which is 788 # triggered when the file is uploaded. This helps avoid getting the 789 # wrong information for multiple upload requests in a row. 790 if "xyz.openbmc_project.Software.Version" in a and \ 791 "xyz.openbmc_project.Software.Activation" not in a: 792 paths.append(path) 793 794 while cls.signal: 795 # Serialize uploads by waiting for the signal to be cleared. 796 # This makes it easier to ensure that the version information 797 # is the right one instead of the data from another upload request. 798 gevent.sleep(1) 799 if not os.path.exists(cls.file_loc): 800 abort(500, "Error Directory not found") 801 paths = [] 802 bus = dbus.SystemBus() 803 cls.signal = bus.add_signal_receiver( 804 signal_callback, 805 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 806 signal_name='InterfacesAdded', 807 path=SOFTWARE_PATH) 808 if not filename: 809 handle, filename = tempfile.mkstemp(cls.file_suffix, 810 cls.file_prefix, cls.file_loc) 811 else: 812 filename = os.path.join(cls.file_loc, filename) 813 handle = os.open(filename, os.O_WRONLY | os.O_CREAT) 814 try: 815 file_contents = request.body.read() 816 request.body.close() 817 os.write(handle, file_contents) 818 # Close file after writing, the image manager process watches for 819 # the close event to know the upload is complete. 820 os.close(handle) 821 except (IOError, ValueError) as e: 822 cleanup() 823 abort(400, str(e)) 824 except Exception: 825 cleanup() 826 abort(400, "Unexpected Error") 827 loop = gobject.MainLoop() 828 gcontext = loop.get_context() 829 count = 0 830 version_id = '' 831 while loop is not None: 832 try: 833 if gcontext.pending(): 834 gcontext.iteration() 835 if not paths: 836 gevent.sleep(1) 837 else: 838 version_id = os.path.basename(paths.pop()) 839 break 840 count += 1 841 if count == 10: 842 break 843 except Exception: 844 break 845 cls.signal.remove() 846 cls.signal = None 847 if version_id: 848 return version_id 849 else: 850 abort(400, "Version already exists or failed to be extracted") 851 852 853class ImagePostHandler(RouteHandler): 854 ''' Handles the /upload/image route. ''' 855 856 verbs = ['POST'] 857 rules = ['/upload/image'] 858 content_type = 'application/octet-stream' 859 860 def __init__(self, app, bus): 861 super(ImagePostHandler, self).__init__( 862 app, bus, self.verbs, self.rules, self.content_type) 863 864 def do_post(self, filename=''): 865 return ImageUploadUtils.do_upload() 866 867 def find(self, **kw): 868 pass 869 870 def setup(self, **kw): 871 pass 872 873 874class EventNotifier: 875 keyNames = {} 876 keyNames['event'] = 'event' 877 keyNames['path'] = 'path' 878 keyNames['intfMap'] = 'interfaces' 879 keyNames['propMap'] = 'properties' 880 keyNames['intf'] = 'interface' 881 882 def __init__(self, wsock, filters): 883 self.wsock = wsock 884 self.paths = filters.get("paths", []) 885 self.interfaces = filters.get("interfaces", []) 886 if not self.paths: 887 self.paths.append(None) 888 bus = dbus.SystemBus() 889 # Add a signal receiver for every path the client is interested in 890 for path in self.paths: 891 bus.add_signal_receiver( 892 self.interfaces_added_handler, 893 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 894 signal_name='InterfacesAdded', 895 path=path) 896 bus.add_signal_receiver( 897 self.properties_changed_handler, 898 dbus_interface=dbus.PROPERTIES_IFACE, 899 signal_name='PropertiesChanged', 900 path=path, 901 path_keyword='path') 902 loop = gobject.MainLoop() 903 # gobject's mainloop.run() will block the entire process, so the gevent 904 # scheduler and hence greenlets won't execute. The while-loop below 905 # works around this limitation by using gevent's sleep, instead of 906 # calling loop.run() 907 gcontext = loop.get_context() 908 while loop is not None: 909 try: 910 if gcontext.pending(): 911 gcontext.iteration() 912 else: 913 # gevent.sleep puts only the current greenlet to sleep, 914 # not the entire process. 915 gevent.sleep(5) 916 except WebSocketError: 917 break 918 919 def interfaces_added_handler(self, path, iprops, **kw): 920 ''' If the client is interested in these changes, respond to the 921 client. This handles d-bus interface additions.''' 922 if (not self.interfaces) or \ 923 (not set(iprops).isdisjoint(self.interfaces)): 924 response = {} 925 response[self.keyNames['event']] = "InterfacesAdded" 926 response[self.keyNames['path']] = path 927 response[self.keyNames['intfMap']] = iprops 928 try: 929 self.wsock.send(json.dumps(response)) 930 except WebSocketError: 931 return 932 933 def properties_changed_handler(self, interface, new, old, **kw): 934 ''' If the client is interested in these changes, respond to the 935 client. This handles d-bus property changes. ''' 936 if (not self.interfaces) or (interface in self.interfaces): 937 path = str(kw['path']) 938 response = {} 939 response[self.keyNames['event']] = "PropertiesChanged" 940 response[self.keyNames['path']] = path 941 response[self.keyNames['intf']] = interface 942 response[self.keyNames['propMap']] = new 943 try: 944 self.wsock.send(json.dumps(response)) 945 except WebSocketError: 946 return 947 948 949class EventHandler(RouteHandler): 950 ''' Handles the /subscribe route, for clients to be able 951 to subscribe to BMC events. ''' 952 953 verbs = ['GET'] 954 rules = ['/subscribe'] 955 956 def __init__(self, app, bus): 957 super(EventHandler, self).__init__( 958 app, bus, self.verbs, self.rules) 959 960 def find(self, **kw): 961 pass 962 963 def setup(self, **kw): 964 pass 965 966 def do_get(self): 967 wsock = request.environ.get('wsgi.websocket') 968 if not wsock: 969 abort(400, 'Expected WebSocket request.') 970 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 971 filters = wsock.receive() 972 filters = json.loads(filters) 973 notifier = EventNotifier(wsock, filters) 974 975class HostConsoleHandler(RouteHandler): 976 ''' Handles the /console route, for clients to be able 977 read/write the host serial console. The way this is 978 done is by exposing a websocket that's mirrored to an 979 abstract UNIX domain socket, which is the source for 980 the console data. ''' 981 982 verbs = ['GET'] 983 # Naming the route console0, because the numbering will help 984 # on multi-bmc/multi-host systems. 985 rules = ['/console0'] 986 987 def __init__(self, app, bus): 988 super(HostConsoleHandler, self).__init__( 989 app, bus, self.verbs, self.rules) 990 991 def find(self, **kw): 992 pass 993 994 def setup(self, **kw): 995 pass 996 997 def read_wsock(self, wsock, sock): 998 while True: 999 try: 1000 incoming = wsock.receive() 1001 if incoming: 1002 # Read websocket, write to UNIX socket 1003 sock.send(incoming) 1004 except Exception as e: 1005 sock.close() 1006 return 1007 1008 def read_sock(self, sock, wsock): 1009 max_sock_read_len = 4096 1010 while True: 1011 try: 1012 outgoing = sock.recv(max_sock_read_len) 1013 if outgoing: 1014 # Read UNIX socket, write to websocket 1015 wsock.send(outgoing) 1016 except Exception as e: 1017 wsock.close() 1018 return 1019 1020 def do_get(self): 1021 wsock = request.environ.get('wsgi.websocket') 1022 if not wsock: 1023 abort(400, 'Expected WebSocket based request.') 1024 1025 # A UNIX domain socket structure defines a 108-byte pathname. The 1026 # server in this case, obmc-console-server, expects a 108-byte path. 1027 socket_name = "\0obmc-console" 1028 trailing_bytes = "\0" * (108 - len(socket_name)) 1029 socket_path = socket_name + trailing_bytes 1030 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1031 1032 try: 1033 sock.connect(socket_path) 1034 except Exception as e: 1035 abort(500, str(e)) 1036 1037 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock) 1038 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock) 1039 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 1040 gevent.joinall([wsock_reader, sock_reader, ping_sender]) 1041 1042 1043class ImagePutHandler(RouteHandler): 1044 ''' Handles the /upload/image/<filename> route. ''' 1045 1046 verbs = ['PUT'] 1047 rules = ['/upload/image/<filename>'] 1048 content_type = 'application/octet-stream' 1049 1050 def __init__(self, app, bus): 1051 super(ImagePutHandler, self).__init__( 1052 app, bus, self.verbs, self.rules, self.content_type) 1053 1054 def do_put(self, filename=''): 1055 return ImageUploadUtils.do_upload(filename) 1056 1057 def find(self, **kw): 1058 pass 1059 1060 def setup(self, **kw): 1061 pass 1062 1063 1064class DownloadDumpHandler(RouteHandler): 1065 ''' Handles the /download/dump route. ''' 1066 1067 verbs = 'GET' 1068 rules = ['/download/dump/<dumpid>'] 1069 content_type = 'application/octet-stream' 1070 dump_loc = '/var/lib/phosphor-debug-collector/dumps' 1071 suppress_json_resp = True 1072 1073 def __init__(self, app, bus): 1074 super(DownloadDumpHandler, self).__init__( 1075 app, bus, self.verbs, self.rules, self.content_type) 1076 1077 def do_get(self, dumpid): 1078 return self.do_download(dumpid) 1079 1080 def find(self, **kw): 1081 pass 1082 1083 def setup(self, **kw): 1084 pass 1085 1086 def do_download(self, dumpid): 1087 dump_loc = os.path.join(self.dump_loc, dumpid) 1088 if not os.path.exists(dump_loc): 1089 abort(404, "Path not found") 1090 1091 files = os.listdir(dump_loc) 1092 num_files = len(files) 1093 if num_files == 0: 1094 abort(404, "Dump not found") 1095 1096 return static_file(os.path.basename(files[0]), root=dump_loc, 1097 download=True, mimetype=self.content_type) 1098 1099 1100class WebHandler(RouteHandler): 1101 ''' Handles the routes for the web UI files. ''' 1102 1103 verbs = 'GET' 1104 1105 # Match only what we know are web files, so everything else 1106 # can get routed to the REST handlers. 1107 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>', 1108 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>', 1109 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>', 1110 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>', 1111 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>', 1112 '/<filename:re:.+\.ico>'] 1113 1114 # The mimetypes module knows about most types, but not these 1115 content_types = { 1116 '.eot': 'application/vnd.ms-fontobject', 1117 '.woff': 'application/x-font-woff', 1118 '.woff2': 'application/x-font-woff2', 1119 '.ttf': 'application/x-font-ttf', 1120 '.map': 'application/json' 1121 } 1122 1123 _require_auth = None 1124 suppress_json_resp = True 1125 1126 def __init__(self, app, bus): 1127 super(WebHandler, self).__init__( 1128 app, bus, self.verbs, self.rules) 1129 1130 def get_type(self, filename): 1131 ''' Returns the content type and encoding for a file ''' 1132 1133 content_type, encoding = mimetypes.guess_type(filename) 1134 1135 # Try our own list if mimetypes didn't recognize it 1136 if content_type is None: 1137 if filename[-3:] == '.gz': 1138 filename = filename[:-3] 1139 extension = filename[filename.rfind('.'):] 1140 content_type = self.content_types.get(extension, None) 1141 1142 return content_type, encoding 1143 1144 def do_get(self, filename='index.html'): 1145 1146 # If a gzipped version exists, use that instead. 1147 # Possible future enhancement: if the client doesn't 1148 # accept compressed files, unzip it ourselves before sending. 1149 if not os.path.exists(os.path.join(www_base_path, filename)): 1150 filename = filename + '.gz' 1151 1152 # Though bottle should protect us, ensure path is valid 1153 realpath = os.path.realpath(filename) 1154 if realpath[0] == '/': 1155 realpath = realpath[1:] 1156 if not os.path.exists(os.path.join(www_base_path, realpath)): 1157 abort(404, "Path not found") 1158 1159 mimetype, encoding = self.get_type(filename) 1160 1161 # Couldn't find the type - let static_file() deal with it, 1162 # though this should never happen. 1163 if mimetype is None: 1164 print("Can't figure out content-type for %s" % filename) 1165 mimetype = 'auto' 1166 1167 # This call will set several header fields for us, 1168 # including the charset if the type is text. 1169 response = static_file(filename, www_base_path, mimetype) 1170 1171 # static_file() will only set the encoding if the 1172 # mimetype was auto, so set it here. 1173 if encoding is not None: 1174 response.set_header('Content-Encoding', encoding) 1175 1176 return response 1177 1178 def find(self, **kw): 1179 pass 1180 1181 def setup(self, **kw): 1182 pass 1183 1184 1185class AuthorizationPlugin(object): 1186 ''' Invokes an optional list of authorization callbacks. ''' 1187 1188 name = 'authorization' 1189 api = 2 1190 1191 class Compose: 1192 def __init__(self, validators, callback, session_mgr): 1193 self.validators = validators 1194 self.callback = callback 1195 self.session_mgr = session_mgr 1196 1197 def __call__(self, *a, **kw): 1198 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) 1199 session = self.session_mgr.get_session(sid) 1200 if request.method != 'OPTIONS': 1201 for x in self.validators: 1202 x(session, *a, **kw) 1203 1204 return self.callback(*a, **kw) 1205 1206 def apply(self, callback, route): 1207 undecorated = route.get_undecorated_callback() 1208 if not isinstance(undecorated, RouteHandler): 1209 return callback 1210 1211 auth_types = getattr( 1212 undecorated, '_require_auth', None) 1213 if not auth_types: 1214 return callback 1215 1216 return self.Compose( 1217 auth_types, callback, undecorated.app.session_handler) 1218 1219 1220class CorsPlugin(object): 1221 ''' Add CORS headers. ''' 1222 1223 name = 'cors' 1224 api = 2 1225 1226 @staticmethod 1227 def process_origin(): 1228 origin = request.headers.get('Origin') 1229 if origin: 1230 response.add_header('Access-Control-Allow-Origin', origin) 1231 response.add_header( 1232 'Access-Control-Allow-Credentials', 'true') 1233 1234 @staticmethod 1235 def process_method_and_headers(verbs): 1236 method = request.headers.get('Access-Control-Request-Method') 1237 headers = request.headers.get('Access-Control-Request-Headers') 1238 if headers: 1239 headers = [x.lower() for x in headers.split(',')] 1240 1241 if method in verbs \ 1242 and headers == ['content-type']: 1243 response.add_header('Access-Control-Allow-Methods', method) 1244 response.add_header( 1245 'Access-Control-Allow-Headers', 'Content-Type') 1246 response.add_header('X-Frame-Options', 'deny') 1247 response.add_header('X-Content-Type-Options', 'nosniff') 1248 response.add_header('X-XSS-Protection', '1; mode=block') 1249 response.add_header( 1250 'Content-Security-Policy', "default-src 'self'") 1251 response.add_header( 1252 'Strict-Transport-Security', 1253 'max-age=31536000; includeSubDomains; preload') 1254 1255 def __init__(self, app): 1256 app.install_error_callback(self.error_callback) 1257 1258 def apply(self, callback, route): 1259 undecorated = route.get_undecorated_callback() 1260 if not isinstance(undecorated, RouteHandler): 1261 return callback 1262 1263 if not getattr(undecorated, '_enable_cors', None): 1264 return callback 1265 1266 def wrap(*a, **kw): 1267 self.process_origin() 1268 self.process_method_and_headers(undecorated._verbs) 1269 return callback(*a, **kw) 1270 1271 return wrap 1272 1273 def error_callback(self, **kw): 1274 self.process_origin() 1275 1276 1277class JsonApiRequestPlugin(object): 1278 ''' Ensures request content satisfies the OpenBMC json api format. ''' 1279 name = 'json_api_request' 1280 api = 2 1281 1282 error_str = "Expecting request format { 'data': <value> }, got '%s'" 1283 type_error_str = "Unsupported Content-Type: '%s'" 1284 json_type = "application/json" 1285 request_methods = ['PUT', 'POST', 'PATCH'] 1286 1287 @staticmethod 1288 def content_expected(): 1289 return request.method in JsonApiRequestPlugin.request_methods 1290 1291 def validate_request(self): 1292 if request.content_length > 0 and \ 1293 request.content_type != self.json_type: 1294 abort(415, self.type_error_str % request.content_type) 1295 1296 try: 1297 request.parameter_list = request.json.get('data') 1298 except ValueError as e: 1299 abort(400, str(e)) 1300 except (AttributeError, KeyError, TypeError): 1301 abort(400, self.error_str % request.json) 1302 1303 def apply(self, callback, route): 1304 content_type = getattr( 1305 route.get_undecorated_callback(), '_content_type', None) 1306 if self.json_type != content_type: 1307 return callback 1308 1309 verbs = getattr( 1310 route.get_undecorated_callback(), '_verbs', None) 1311 if verbs is None: 1312 return callback 1313 1314 if not set(self.request_methods).intersection(verbs): 1315 return callback 1316 1317 def wrap(*a, **kw): 1318 if self.content_expected(): 1319 self.validate_request() 1320 return callback(*a, **kw) 1321 1322 return wrap 1323 1324 1325class JsonApiRequestTypePlugin(object): 1326 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1327 name = 'json_api_method_request' 1328 api = 2 1329 1330 error_str = "Expecting request format { 'data': %s }, got '%s'" 1331 json_type = "application/json" 1332 1333 def apply(self, callback, route): 1334 content_type = getattr( 1335 route.get_undecorated_callback(), '_content_type', None) 1336 if self.json_type != content_type: 1337 return callback 1338 1339 request_type = getattr( 1340 route.get_undecorated_callback(), 'request_type', None) 1341 if request_type is None: 1342 return callback 1343 1344 def validate_request(): 1345 if not isinstance(request.parameter_list, request_type): 1346 abort(400, self.error_str % (str(request_type), request.json)) 1347 1348 def wrap(*a, **kw): 1349 if JsonApiRequestPlugin.content_expected(): 1350 validate_request() 1351 return callback(*a, **kw) 1352 1353 return wrap 1354 1355 1356class JsonErrorsPlugin(JSONPlugin): 1357 ''' Extend the Bottle JSONPlugin such that it also encodes error 1358 responses. ''' 1359 1360 def __init__(self, app, **kw): 1361 super(JsonErrorsPlugin, self).__init__(**kw) 1362 self.json_opts = { 1363 x: y for x, y in kw.items() 1364 if x in ['indent', 'sort_keys']} 1365 app.install_error_callback(self.error_callback) 1366 1367 def error_callback(self, response_object, response_body, **kw): 1368 response_body['body'] = json.dumps(response_object, **self.json_opts) 1369 response.content_type = 'application/json' 1370 1371 1372class JsonApiResponsePlugin(object): 1373 ''' Emits responses in the OpenBMC json api format. ''' 1374 name = 'json_api_response' 1375 api = 2 1376 1377 @staticmethod 1378 def has_body(): 1379 return request.method not in ['OPTIONS'] 1380 1381 def __init__(self, app): 1382 app.install_error_callback(self.error_callback) 1383 1384 def apply(self, callback, route): 1385 skip = getattr( 1386 route.get_undecorated_callback(), 'suppress_json_resp', None) 1387 if skip: 1388 return callback 1389 1390 def wrap(*a, **kw): 1391 data = callback(*a, **kw) 1392 if self.has_body(): 1393 resp = {'data': data} 1394 resp['status'] = 'ok' 1395 resp['message'] = response.status_line 1396 return resp 1397 return wrap 1398 1399 def error_callback(self, error, response_object, **kw): 1400 response_object['message'] = error.status_line 1401 response_object['status'] = 'error' 1402 response_object.setdefault('data', {})['description'] = str(error.body) 1403 if error.status_code == 500: 1404 response_object['data']['exception'] = repr(error.exception) 1405 response_object['data']['traceback'] = error.traceback.splitlines() 1406 1407 1408class JsonpPlugin(object): 1409 ''' Json javascript wrapper. ''' 1410 name = 'jsonp' 1411 api = 2 1412 1413 def __init__(self, app, **kw): 1414 app.install_error_callback(self.error_callback) 1415 1416 @staticmethod 1417 def to_jsonp(json): 1418 jwrapper = request.query.callback or None 1419 if(jwrapper): 1420 response.set_header('Content-Type', 'application/javascript') 1421 json = jwrapper + '(' + json + ');' 1422 return json 1423 1424 def apply(self, callback, route): 1425 def wrap(*a, **kw): 1426 return self.to_jsonp(callback(*a, **kw)) 1427 return wrap 1428 1429 def error_callback(self, response_body, **kw): 1430 response_body['body'] = self.to_jsonp(response_body['body']) 1431 1432 1433class ContentCheckerPlugin(object): 1434 ''' Ensures that a route is associated with the expected content-type 1435 header. ''' 1436 name = 'content_checker' 1437 api = 2 1438 1439 class Checker: 1440 def __init__(self, type, callback): 1441 self.expected_type = type 1442 self.callback = callback 1443 self.error_str = "Expecting content type '%s', got '%s'" 1444 1445 def __call__(self, *a, **kw): 1446 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1447 self.expected_type and \ 1448 self.expected_type != request.content_type: 1449 abort(415, self.error_str % (self.expected_type, 1450 request.content_type)) 1451 1452 return self.callback(*a, **kw) 1453 1454 def apply(self, callback, route): 1455 content_type = getattr( 1456 route.get_undecorated_callback(), '_content_type', None) 1457 1458 return self.Checker(content_type, callback) 1459 1460 1461class App(Bottle): 1462 def __init__(self, **kw): 1463 super(App, self).__init__(autojson=False) 1464 1465 self.have_wsock = kw.get('have_wsock', False) 1466 self.with_bmc_check = '--with-bmc-check' in sys.argv 1467 1468 self.bus = dbus.SystemBus() 1469 self.mapper = obmc.mapper.Mapper(self.bus) 1470 self.error_callbacks = [] 1471 1472 self.install_hooks() 1473 self.install_plugins() 1474 self.create_handlers() 1475 self.install_handlers() 1476 1477 def install_plugins(self): 1478 # install json api plugins 1479 json_kw = {'indent': 2, 'sort_keys': True} 1480 self.install(AuthorizationPlugin()) 1481 self.install(CorsPlugin(self)) 1482 self.install(ContentCheckerPlugin()) 1483 self.install(JsonpPlugin(self, **json_kw)) 1484 self.install(JsonErrorsPlugin(self, **json_kw)) 1485 self.install(JsonApiResponsePlugin(self)) 1486 self.install(JsonApiRequestPlugin()) 1487 self.install(JsonApiRequestTypePlugin()) 1488 1489 def install_hooks(self): 1490 self.error_handler_type = type(self.default_error_handler) 1491 self.original_error_handler = self.default_error_handler 1492 self.default_error_handler = self.error_handler_type( 1493 self.custom_error_handler, self, Bottle) 1494 1495 self.real_router_match = self.router.match 1496 self.router.match = self.custom_router_match 1497 self.add_hook('before_request', self.strip_extra_slashes) 1498 1499 def create_handlers(self): 1500 # create route handlers 1501 self.session_handler = SessionHandler(self, self.bus) 1502 self.web_handler = WebHandler(self, self.bus) 1503 self.directory_handler = DirectoryHandler(self, self.bus) 1504 self.list_names_handler = ListNamesHandler(self, self.bus) 1505 self.list_handler = ListHandler(self, self.bus) 1506 self.method_handler = MethodHandler(self, self.bus) 1507 self.property_handler = PropertyHandler(self, self.bus) 1508 self.schema_handler = SchemaHandler(self, self.bus) 1509 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1510 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1511 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1512 if self.have_wsock: 1513 self.event_handler = EventHandler(self, self.bus) 1514 self.host_console_handler = HostConsoleHandler(self, self.bus) 1515 self.instance_handler = InstanceHandler(self, self.bus) 1516 1517 def install_handlers(self): 1518 self.session_handler.install() 1519 self.web_handler.install() 1520 self.directory_handler.install() 1521 self.list_names_handler.install() 1522 self.list_handler.install() 1523 self.method_handler.install() 1524 self.property_handler.install() 1525 self.schema_handler.install() 1526 self.image_upload_post_handler.install() 1527 self.image_upload_put_handler.install() 1528 self.download_dump_get_handler.install() 1529 if self.have_wsock: 1530 self.event_handler.install() 1531 self.host_console_handler.install() 1532 # this has to come last, since it matches everything 1533 self.instance_handler.install() 1534 1535 def install_error_callback(self, callback): 1536 self.error_callbacks.insert(0, callback) 1537 1538 def custom_router_match(self, environ): 1539 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1540 needed doesn't work for us since the instance rules match 1541 everything. This monkey-patch lets the route handler figure 1542 out which response is needed. This could be accomplished 1543 with a hook but that would require calling the router match 1544 function twice. 1545 ''' 1546 route, args = self.real_router_match(environ) 1547 if isinstance(route.callback, RouteHandler): 1548 route.callback._setup(**args) 1549 1550 return route, args 1551 1552 def custom_error_handler(self, res, error): 1553 ''' Allow plugins to modify error responses too via this custom 1554 error handler. ''' 1555 1556 response_object = {} 1557 response_body = {} 1558 for x in self.error_callbacks: 1559 x(error=error, 1560 response_object=response_object, 1561 response_body=response_body) 1562 1563 return response_body.get('body', "") 1564 1565 @staticmethod 1566 def strip_extra_slashes(): 1567 path = request.environ['PATH_INFO'] 1568 trailing = ("", "/")[path[-1] == '/'] 1569 parts = list(filter(bool, path.split('/'))) 1570 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1571