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