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 self.signals = [] 971 self.socket_error = False 972 if not self.paths: 973 self.paths.append(None) 974 bus = dbus.SystemBus() 975 # Add a signal receiver for every path the client is interested in 976 for path in self.paths: 977 add_sig = bus.add_signal_receiver( 978 self.interfaces_added_handler, 979 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager', 980 signal_name='InterfacesAdded', 981 path=path) 982 chg_sig = bus.add_signal_receiver( 983 self.properties_changed_handler, 984 dbus_interface=dbus.PROPERTIES_IFACE, 985 signal_name='PropertiesChanged', 986 path=path, 987 path_keyword='path') 988 self.signals.append(add_sig) 989 self.signals.append(chg_sig) 990 loop = gobject.MainLoop() 991 # gobject's mainloop.run() will block the entire process, so the gevent 992 # scheduler and hence greenlets won't execute. The while-loop below 993 # works around this limitation by using gevent's sleep, instead of 994 # calling loop.run() 995 gcontext = loop.get_context() 996 while loop is not None: 997 try: 998 if self.socket_error: 999 for signal in self.signals: 1000 signal.remove() 1001 loop.quit() 1002 break; 1003 if gcontext.pending(): 1004 gcontext.iteration() 1005 else: 1006 # gevent.sleep puts only the current greenlet to sleep, 1007 # not the entire process. 1008 gevent.sleep(5) 1009 except WebSocketError: 1010 break 1011 1012 def interfaces_added_handler(self, path, iprops, **kw): 1013 ''' If the client is interested in these changes, respond to the 1014 client. This handles d-bus interface additions.''' 1015 if (not self.interfaces) or \ 1016 (not set(iprops).isdisjoint(self.interfaces)): 1017 response = {} 1018 response[self.keyNames['event']] = "InterfacesAdded" 1019 response[self.keyNames['path']] = path 1020 response[self.keyNames['intfMap']] = iprops 1021 try: 1022 self.wsock.send(json.dumps(response)) 1023 except: 1024 self.socket_error = True 1025 return 1026 1027 def properties_changed_handler(self, interface, new, old, **kw): 1028 ''' If the client is interested in these changes, respond to the 1029 client. This handles d-bus property changes. ''' 1030 if (not self.interfaces) or (interface in self.interfaces): 1031 path = str(kw['path']) 1032 response = {} 1033 response[self.keyNames['event']] = "PropertiesChanged" 1034 response[self.keyNames['path']] = path 1035 response[self.keyNames['intf']] = interface 1036 response[self.keyNames['propMap']] = new 1037 try: 1038 self.wsock.send(json.dumps(response)) 1039 except: 1040 self.socket_error = True 1041 return 1042 1043 1044class EventHandler(RouteHandler): 1045 ''' Handles the /subscribe route, for clients to be able 1046 to subscribe to BMC events. ''' 1047 1048 verbs = ['GET'] 1049 rules = ['/subscribe'] 1050 suppress_logging = True 1051 1052 def __init__(self, app, bus): 1053 super(EventHandler, self).__init__( 1054 app, bus, self.verbs, self.rules) 1055 1056 def find(self, **kw): 1057 pass 1058 1059 def setup(self, **kw): 1060 pass 1061 1062 def do_get(self): 1063 wsock = request.environ.get('wsgi.websocket') 1064 if not wsock: 1065 abort(400, 'Expected WebSocket request.') 1066 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 1067 filters = wsock.receive() 1068 filters = json.loads(filters) 1069 notifier = EventNotifier(wsock, filters) 1070 1071class HostConsoleHandler(RouteHandler): 1072 ''' Handles the /console route, for clients to be able 1073 read/write the host serial console. The way this is 1074 done is by exposing a websocket that's mirrored to an 1075 abstract UNIX domain socket, which is the source for 1076 the console data. ''' 1077 1078 verbs = ['GET'] 1079 # Naming the route console0, because the numbering will help 1080 # on multi-bmc/multi-host systems. 1081 rules = ['/console0'] 1082 suppress_logging = True 1083 1084 def __init__(self, app, bus): 1085 super(HostConsoleHandler, self).__init__( 1086 app, bus, self.verbs, self.rules) 1087 1088 def find(self, **kw): 1089 pass 1090 1091 def setup(self, **kw): 1092 pass 1093 1094 def read_wsock(self, wsock, sock): 1095 while True: 1096 try: 1097 incoming = wsock.receive() 1098 if incoming: 1099 # Read websocket, write to UNIX socket 1100 sock.send(incoming) 1101 except Exception as e: 1102 sock.close() 1103 return 1104 1105 def read_sock(self, sock, wsock): 1106 max_sock_read_len = 4096 1107 while True: 1108 try: 1109 outgoing = sock.recv(max_sock_read_len) 1110 if outgoing: 1111 # Read UNIX socket, write to websocket 1112 wsock.send(outgoing) 1113 except Exception as e: 1114 wsock.close() 1115 return 1116 1117 def do_get(self): 1118 wsock = request.environ.get('wsgi.websocket') 1119 if not wsock: 1120 abort(400, 'Expected WebSocket based request.') 1121 1122 # A UNIX domain socket structure defines a 108-byte pathname. The 1123 # server in this case, obmc-console-server, expects a 108-byte path. 1124 socket_name = "\0obmc-console" 1125 trailing_bytes = "\0" * (108 - len(socket_name)) 1126 socket_path = socket_name + trailing_bytes 1127 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 1128 1129 try: 1130 sock.connect(socket_path) 1131 except Exception as e: 1132 abort(500, str(e)) 1133 1134 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock) 1135 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock) 1136 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT) 1137 gevent.joinall([wsock_reader, sock_reader, ping_sender]) 1138 1139 1140class ImagePutHandler(RouteHandler): 1141 ''' Handles the /upload/image/<filename> route. ''' 1142 1143 verbs = ['PUT'] 1144 rules = ['/upload/image/<filename>'] 1145 content_type = 'application/octet-stream' 1146 1147 def __init__(self, app, bus): 1148 super(ImagePutHandler, self).__init__( 1149 app, bus, self.verbs, self.rules, self.content_type) 1150 1151 def do_put(self, filename=''): 1152 return ImageUploadUtils.do_upload(filename) 1153 1154 def find(self, **kw): 1155 pass 1156 1157 def setup(self, **kw): 1158 pass 1159 1160 1161class DownloadDumpHandler(RouteHandler): 1162 ''' Handles the /download/dump route. ''' 1163 1164 verbs = 'GET' 1165 rules = ['/download/dump/<dumpid>'] 1166 content_type = 'application/octet-stream' 1167 dump_loc = '/var/lib/phosphor-debug-collector/dumps' 1168 suppress_json_resp = True 1169 suppress_logging = True 1170 1171 def __init__(self, app, bus): 1172 super(DownloadDumpHandler, self).__init__( 1173 app, bus, self.verbs, self.rules, self.content_type) 1174 1175 def do_get(self, dumpid): 1176 return self.do_download(dumpid) 1177 1178 def find(self, **kw): 1179 pass 1180 1181 def setup(self, **kw): 1182 pass 1183 1184 def do_download(self, dumpid): 1185 dump_loc = os.path.join(self.dump_loc, dumpid) 1186 if not os.path.exists(dump_loc): 1187 abort(404, "Path not found") 1188 1189 files = os.listdir(dump_loc) 1190 num_files = len(files) 1191 if num_files == 0: 1192 abort(404, "Dump not found") 1193 1194 return static_file(os.path.basename(files[0]), root=dump_loc, 1195 download=True, mimetype=self.content_type) 1196 1197 1198class WebHandler(RouteHandler): 1199 ''' Handles the routes for the web UI files. ''' 1200 1201 verbs = 'GET' 1202 1203 # Match only what we know are web files, so everything else 1204 # can get routed to the REST handlers. 1205 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>', 1206 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>', 1207 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>', 1208 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>', 1209 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>', 1210 '/<filename:re:.+\.ico>'] 1211 1212 # The mimetypes module knows about most types, but not these 1213 content_types = { 1214 '.eot': 'application/vnd.ms-fontobject', 1215 '.woff': 'application/x-font-woff', 1216 '.woff2': 'application/x-font-woff2', 1217 '.ttf': 'application/x-font-ttf', 1218 '.map': 'application/json' 1219 } 1220 1221 _require_auth = None 1222 suppress_json_resp = True 1223 suppress_logging = True 1224 1225 def __init__(self, app, bus): 1226 super(WebHandler, self).__init__( 1227 app, bus, self.verbs, self.rules) 1228 1229 def get_type(self, filename): 1230 ''' Returns the content type and encoding for a file ''' 1231 1232 content_type, encoding = mimetypes.guess_type(filename) 1233 1234 # Try our own list if mimetypes didn't recognize it 1235 if content_type is None: 1236 if filename[-3:] == '.gz': 1237 filename = filename[:-3] 1238 extension = filename[filename.rfind('.'):] 1239 content_type = self.content_types.get(extension, None) 1240 1241 return content_type, encoding 1242 1243 def do_get(self, filename='index.html'): 1244 1245 # If a gzipped version exists, use that instead. 1246 # Possible future enhancement: if the client doesn't 1247 # accept compressed files, unzip it ourselves before sending. 1248 if not os.path.exists(os.path.join(www_base_path, filename)): 1249 filename = filename + '.gz' 1250 1251 # Though bottle should protect us, ensure path is valid 1252 realpath = os.path.realpath(filename) 1253 if realpath[0] == '/': 1254 realpath = realpath[1:] 1255 if not os.path.exists(os.path.join(www_base_path, realpath)): 1256 abort(404, "Path not found") 1257 1258 mimetype, encoding = self.get_type(filename) 1259 1260 # Couldn't find the type - let static_file() deal with it, 1261 # though this should never happen. 1262 if mimetype is None: 1263 print("Can't figure out content-type for %s" % filename) 1264 mimetype = 'auto' 1265 1266 # This call will set several header fields for us, 1267 # including the charset if the type is text. 1268 response = static_file(filename, www_base_path, mimetype) 1269 1270 # static_file() will only set the encoding if the 1271 # mimetype was auto, so set it here. 1272 if encoding is not None: 1273 response.set_header('Content-Encoding', encoding) 1274 1275 return response 1276 1277 def find(self, **kw): 1278 pass 1279 1280 def setup(self, **kw): 1281 pass 1282 1283 1284class AuthorizationPlugin(object): 1285 ''' Invokes an optional list of authorization callbacks. ''' 1286 1287 name = 'authorization' 1288 api = 2 1289 1290 class Compose: 1291 def __init__(self, validators, callback, session_mgr): 1292 self.validators = validators 1293 self.callback = callback 1294 self.session_mgr = session_mgr 1295 1296 def __call__(self, *a, **kw): 1297 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key) 1298 session = self.session_mgr.get_session(sid) 1299 if request.method != 'OPTIONS': 1300 for x in self.validators: 1301 x(session, *a, **kw) 1302 1303 return self.callback(*a, **kw) 1304 1305 def apply(self, callback, route): 1306 undecorated = route.get_undecorated_callback() 1307 if not isinstance(undecorated, RouteHandler): 1308 return callback 1309 1310 auth_types = getattr( 1311 undecorated, '_require_auth', None) 1312 if not auth_types: 1313 return callback 1314 1315 return self.Compose( 1316 auth_types, callback, undecorated.app.session_handler) 1317 1318 1319class CorsPlugin(object): 1320 ''' Add CORS headers. ''' 1321 1322 name = 'cors' 1323 api = 2 1324 1325 @staticmethod 1326 def process_origin(): 1327 origin = request.headers.get('Origin') 1328 if origin: 1329 response.add_header('Access-Control-Allow-Origin', origin) 1330 response.add_header( 1331 'Access-Control-Allow-Credentials', 'true') 1332 1333 @staticmethod 1334 def process_method_and_headers(verbs): 1335 method = request.headers.get('Access-Control-Request-Method') 1336 headers = request.headers.get('Access-Control-Request-Headers') 1337 if headers: 1338 headers = [x.lower() for x in headers.split(',')] 1339 1340 if method in verbs \ 1341 and headers == ['content-type']: 1342 response.add_header('Access-Control-Allow-Methods', method) 1343 response.add_header( 1344 'Access-Control-Allow-Headers', 'Content-Type') 1345 response.add_header('X-Frame-Options', 'deny') 1346 response.add_header('X-Content-Type-Options', 'nosniff') 1347 response.add_header('X-XSS-Protection', '1; mode=block') 1348 response.add_header( 1349 'Content-Security-Policy', "default-src 'self'") 1350 response.add_header( 1351 'Strict-Transport-Security', 1352 'max-age=31536000; includeSubDomains; preload') 1353 1354 def __init__(self, app): 1355 app.install_error_callback(self.error_callback) 1356 1357 def apply(self, callback, route): 1358 undecorated = route.get_undecorated_callback() 1359 if not isinstance(undecorated, RouteHandler): 1360 return callback 1361 1362 if not getattr(undecorated, '_enable_cors', None): 1363 return callback 1364 1365 def wrap(*a, **kw): 1366 self.process_origin() 1367 self.process_method_and_headers(undecorated._verbs) 1368 return callback(*a, **kw) 1369 1370 return wrap 1371 1372 def error_callback(self, **kw): 1373 self.process_origin() 1374 1375 1376class JsonApiRequestPlugin(object): 1377 ''' Ensures request content satisfies the OpenBMC json api format. ''' 1378 name = 'json_api_request' 1379 api = 2 1380 1381 error_str = "Expecting request format { 'data': <value> }, got '%s'" 1382 type_error_str = "Unsupported Content-Type: '%s'" 1383 json_type = "application/json" 1384 request_methods = ['PUT', 'POST', 'PATCH'] 1385 1386 @staticmethod 1387 def content_expected(): 1388 return request.method in JsonApiRequestPlugin.request_methods 1389 1390 def validate_request(self): 1391 if request.content_length > 0 and \ 1392 request.content_type != self.json_type: 1393 abort(415, self.type_error_str % request.content_type) 1394 1395 try: 1396 request.parameter_list = request.json.get('data') 1397 except ValueError as e: 1398 abort(400, str(e)) 1399 except (AttributeError, KeyError, TypeError): 1400 abort(400, self.error_str % request.json) 1401 1402 def apply(self, callback, route): 1403 content_type = getattr( 1404 route.get_undecorated_callback(), '_content_type', None) 1405 if self.json_type != content_type: 1406 return callback 1407 1408 verbs = getattr( 1409 route.get_undecorated_callback(), '_verbs', None) 1410 if verbs is None: 1411 return callback 1412 1413 if not set(self.request_methods).intersection(verbs): 1414 return callback 1415 1416 def wrap(*a, **kw): 1417 if self.content_expected(): 1418 self.validate_request() 1419 return callback(*a, **kw) 1420 1421 return wrap 1422 1423 1424class JsonApiRequestTypePlugin(object): 1425 ''' Ensures request content type satisfies the OpenBMC json api format. ''' 1426 name = 'json_api_method_request' 1427 api = 2 1428 1429 error_str = "Expecting request format { 'data': %s }, got '%s'" 1430 json_type = "application/json" 1431 1432 def apply(self, callback, route): 1433 content_type = getattr( 1434 route.get_undecorated_callback(), '_content_type', None) 1435 if self.json_type != content_type: 1436 return callback 1437 1438 request_type = getattr( 1439 route.get_undecorated_callback(), 'request_type', None) 1440 if request_type is None: 1441 return callback 1442 1443 def validate_request(): 1444 if not isinstance(request.parameter_list, request_type): 1445 abort(400, self.error_str % (str(request_type), request.json)) 1446 1447 def wrap(*a, **kw): 1448 if JsonApiRequestPlugin.content_expected(): 1449 validate_request() 1450 return callback(*a, **kw) 1451 1452 return wrap 1453 1454 1455class JsonErrorsPlugin(JSONPlugin): 1456 ''' Extend the Bottle JSONPlugin such that it also encodes error 1457 responses. ''' 1458 1459 def __init__(self, app, **kw): 1460 super(JsonErrorsPlugin, self).__init__(**kw) 1461 self.json_opts = { 1462 x: y for x, y in kw.items() 1463 if x in ['indent', 'sort_keys']} 1464 app.install_error_callback(self.error_callback) 1465 1466 def error_callback(self, response_object, response_body, **kw): 1467 response_body['body'] = json.dumps(response_object, **self.json_opts) 1468 response.content_type = 'application/json' 1469 1470 1471class JsonApiResponsePlugin(object): 1472 ''' Emits responses in the OpenBMC json api format. ''' 1473 name = 'json_api_response' 1474 api = 2 1475 1476 @staticmethod 1477 def has_body(): 1478 return request.method not in ['OPTIONS'] 1479 1480 def __init__(self, app): 1481 app.install_error_callback(self.error_callback) 1482 1483 @staticmethod 1484 def dbus_boolean_to_bool(data): 1485 ''' Convert all dbus.Booleans to true/false instead of 1/0 as 1486 the JSON encoder thinks they're ints. Note that unlike 1487 dicts and lists, tuples (from a dbus.Struct) are immutable 1488 so they need special handling. ''' 1489 1490 def walkdict(data): 1491 for key, value in data.items(): 1492 if isinstance(value, dbus.Boolean): 1493 data[key] = bool(value) 1494 elif isinstance(value, tuple): 1495 data[key] = walktuple(value) 1496 else: 1497 JsonApiResponsePlugin.dbus_boolean_to_bool(value) 1498 1499 def walklist(data): 1500 for i in range(len(data)): 1501 if isinstance(data[i], dbus.Boolean): 1502 data[i] = bool(data[i]) 1503 elif isinstance(data[i], tuple): 1504 data[i] = walktuple(data[i]) 1505 else: 1506 JsonApiResponsePlugin.dbus_boolean_to_bool(data[i]) 1507 1508 def walktuple(data): 1509 new = [] 1510 for item in data: 1511 if isinstance(item, dbus.Boolean): 1512 item = bool(item) 1513 else: 1514 JsonApiResponsePlugin.dbus_boolean_to_bool(item) 1515 new.append(item) 1516 return tuple(new) 1517 1518 if isinstance(data, dict): 1519 walkdict(data) 1520 elif isinstance(data, list): 1521 walklist(data) 1522 1523 def apply(self, callback, route): 1524 skip = getattr( 1525 route.get_undecorated_callback(), 'suppress_json_resp', None) 1526 if skip: 1527 return callback 1528 1529 def wrap(*a, **kw): 1530 data = callback(*a, **kw) 1531 JsonApiResponsePlugin.dbus_boolean_to_bool(data) 1532 if self.has_body(): 1533 resp = {'data': data} 1534 resp['status'] = 'ok' 1535 resp['message'] = response.status_line 1536 return resp 1537 return wrap 1538 1539 def error_callback(self, error, response_object, **kw): 1540 response_object['message'] = error.status_line 1541 response_object['status'] = 'error' 1542 response_object.setdefault('data', {})['description'] = str(error.body) 1543 if error.status_code == 500: 1544 response_object['data']['exception'] = repr(error.exception) 1545 response_object['data']['traceback'] = error.traceback.splitlines() 1546 1547 1548class JsonpPlugin(object): 1549 ''' Json javascript wrapper. ''' 1550 name = 'jsonp' 1551 api = 2 1552 1553 def __init__(self, app, **kw): 1554 app.install_error_callback(self.error_callback) 1555 1556 @staticmethod 1557 def to_jsonp(json): 1558 jwrapper = request.query.callback or None 1559 if(jwrapper): 1560 response.set_header('Content-Type', 'application/javascript') 1561 json = jwrapper + '(' + json + ');' 1562 return json 1563 1564 def apply(self, callback, route): 1565 def wrap(*a, **kw): 1566 return self.to_jsonp(callback(*a, **kw)) 1567 return wrap 1568 1569 def error_callback(self, response_body, **kw): 1570 response_body['body'] = self.to_jsonp(response_body['body']) 1571 1572 1573class ContentCheckerPlugin(object): 1574 ''' Ensures that a route is associated with the expected content-type 1575 header. ''' 1576 name = 'content_checker' 1577 api = 2 1578 1579 class Checker: 1580 def __init__(self, type, callback): 1581 self.expected_type = type 1582 self.callback = callback 1583 self.error_str = "Expecting content type '%s', got '%s'" 1584 1585 def __call__(self, *a, **kw): 1586 if request.method in ['PUT', 'POST', 'PATCH'] and \ 1587 self.expected_type and \ 1588 self.expected_type != request.content_type: 1589 abort(415, self.error_str % (self.expected_type, 1590 request.content_type)) 1591 1592 return self.callback(*a, **kw) 1593 1594 def apply(self, callback, route): 1595 content_type = getattr( 1596 route.get_undecorated_callback(), '_content_type', None) 1597 1598 return self.Checker(content_type, callback) 1599 1600 1601class LoggingPlugin(object): 1602 ''' Wraps a request in order to emit a log after the request is handled. ''' 1603 name = 'loggingp' 1604 api = 2 1605 1606 class Logger: 1607 def __init__(self, suppress_json_logging, callback, app): 1608 self.suppress_json_logging = suppress_json_logging 1609 self.callback = callback 1610 self.app = app 1611 self.logging_enabled = None 1612 self.bus = dbus.SystemBus() 1613 self.dbus_path = '/xyz/openbmc_project/logging/rest_api_logs' 1614 self.no_json = [ 1615 '/xyz/openbmc_project/user/ldap/action/CreateConfig' 1616 ] 1617 self.bus.add_signal_receiver( 1618 self.properties_changed_handler, 1619 dbus_interface=dbus.PROPERTIES_IFACE, 1620 signal_name='PropertiesChanged', 1621 path=self.dbus_path) 1622 Greenlet.spawn(self.dbus_loop) 1623 1624 def __call__(self, *a, **kw): 1625 resp = self.callback(*a, **kw) 1626 if not self.enabled(): 1627 return resp 1628 if request.method == 'GET': 1629 return resp 1630 json = request.json 1631 if self.suppress_json_logging: 1632 json = None 1633 elif any(substring in request.url for substring in self.no_json): 1634 json = None 1635 session = self.app.session_handler.get_session_from_cookie() 1636 user = None 1637 if "/login" in request.url: 1638 user = request.parameter_list[0] 1639 elif session is not None: 1640 user = session['user'] 1641 print("{remote} user:{user} {method} {url} json:{json} {status}" \ 1642 .format( 1643 user=user, 1644 remote=request.remote_addr, 1645 method=request.method, 1646 url=request.url, 1647 json=json, 1648 status=response.status)) 1649 return resp 1650 1651 def enabled(self): 1652 if self.logging_enabled is None: 1653 try: 1654 obj = self.bus.get_object( 1655 'xyz.openbmc_project.Settings', 1656 self.dbus_path) 1657 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE) 1658 logging_enabled = iface.Get( 1659 'xyz.openbmc_project.Object.Enable', 1660 'Enabled') 1661 self.logging_enabled = logging_enabled 1662 except dbus.exceptions.DBusException: 1663 self.logging_enabled = False 1664 return self.logging_enabled 1665 1666 def dbus_loop(self): 1667 loop = gobject.MainLoop() 1668 gcontext = loop.get_context() 1669 while loop is not None: 1670 try: 1671 if gcontext.pending(): 1672 gcontext.iteration() 1673 else: 1674 gevent.sleep(5) 1675 except Exception as e: 1676 break 1677 1678 def properties_changed_handler(self, interface, new, old, **kw): 1679 self.logging_enabled = new.values()[0] 1680 1681 def apply(self, callback, route): 1682 cb = route.get_undecorated_callback() 1683 skip = getattr( 1684 cb, 'suppress_logging', None) 1685 if skip: 1686 return callback 1687 1688 suppress_json_logging = getattr( 1689 cb, 'suppress_json_logging', None) 1690 return self.Logger(suppress_json_logging, callback, cb.app) 1691 1692 1693class App(Bottle): 1694 def __init__(self, **kw): 1695 super(App, self).__init__(autojson=False) 1696 1697 self.have_wsock = kw.get('have_wsock', False) 1698 self.with_bmc_check = '--with-bmc-check' in sys.argv 1699 1700 self.bus = dbus.SystemBus() 1701 self.mapper = obmc.mapper.Mapper(self.bus) 1702 self.error_callbacks = [] 1703 1704 self.install_hooks() 1705 self.install_plugins() 1706 self.create_handlers() 1707 self.install_handlers() 1708 1709 def install_plugins(self): 1710 # install json api plugins 1711 json_kw = {'indent': 2, 'sort_keys': True} 1712 self.install(AuthorizationPlugin()) 1713 self.install(CorsPlugin(self)) 1714 self.install(ContentCheckerPlugin()) 1715 self.install(JsonpPlugin(self, **json_kw)) 1716 self.install(JsonErrorsPlugin(self, **json_kw)) 1717 self.install(JsonApiResponsePlugin(self)) 1718 self.install(JsonApiRequestPlugin()) 1719 self.install(JsonApiRequestTypePlugin()) 1720 self.install(LoggingPlugin()) 1721 1722 def install_hooks(self): 1723 self.error_handler_type = type(self.default_error_handler) 1724 self.original_error_handler = self.default_error_handler 1725 self.default_error_handler = self.error_handler_type( 1726 self.custom_error_handler, self, Bottle) 1727 1728 self.real_router_match = self.router.match 1729 self.router.match = self.custom_router_match 1730 self.add_hook('before_request', self.strip_extra_slashes) 1731 1732 def create_handlers(self): 1733 # create route handlers 1734 self.session_handler = SessionHandler(self, self.bus) 1735 self.web_handler = WebHandler(self, self.bus) 1736 self.directory_handler = DirectoryHandler(self, self.bus) 1737 self.list_names_handler = ListNamesHandler(self, self.bus) 1738 self.list_handler = ListHandler(self, self.bus) 1739 self.method_handler = MethodHandler(self, self.bus) 1740 self.property_handler = PropertyHandler(self, self.bus) 1741 self.schema_handler = SchemaHandler(self, self.bus) 1742 self.image_upload_post_handler = ImagePostHandler(self, self.bus) 1743 self.image_upload_put_handler = ImagePutHandler(self, self.bus) 1744 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus) 1745 self.certificate_put_handler = CertificatePutHandler(self, self.bus) 1746 if self.have_wsock: 1747 self.event_handler = EventHandler(self, self.bus) 1748 self.host_console_handler = HostConsoleHandler(self, self.bus) 1749 self.instance_handler = InstanceHandler(self, self.bus) 1750 1751 def install_handlers(self): 1752 self.session_handler.install() 1753 self.web_handler.install() 1754 self.directory_handler.install() 1755 self.list_names_handler.install() 1756 self.list_handler.install() 1757 self.method_handler.install() 1758 self.property_handler.install() 1759 self.schema_handler.install() 1760 self.image_upload_post_handler.install() 1761 self.image_upload_put_handler.install() 1762 self.download_dump_get_handler.install() 1763 self.certificate_put_handler.install() 1764 if self.have_wsock: 1765 self.event_handler.install() 1766 self.host_console_handler.install() 1767 # this has to come last, since it matches everything 1768 self.instance_handler.install() 1769 1770 def install_error_callback(self, callback): 1771 self.error_callbacks.insert(0, callback) 1772 1773 def custom_router_match(self, environ): 1774 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is 1775 needed doesn't work for us since the instance rules match 1776 everything. This monkey-patch lets the route handler figure 1777 out which response is needed. This could be accomplished 1778 with a hook but that would require calling the router match 1779 function twice. 1780 ''' 1781 route, args = self.real_router_match(environ) 1782 if isinstance(route.callback, RouteHandler): 1783 route.callback._setup(**args) 1784 1785 return route, args 1786 1787 def custom_error_handler(self, res, error): 1788 ''' Allow plugins to modify error responses too via this custom 1789 error handler. ''' 1790 1791 response_object = {} 1792 response_body = {} 1793 for x in self.error_callbacks: 1794 x(error=error, 1795 response_object=response_object, 1796 response_body=response_body) 1797 1798 return response_body.get('body', "") 1799 1800 @staticmethod 1801 def strip_extra_slashes(): 1802 path = request.environ['PATH_INFO'] 1803 trailing = ("", "/")[path[-1] == '/'] 1804 parts = list(filter(bool, path.split('/'))) 1805 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing 1806