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