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