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