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