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