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