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            try:
534                info = self.get_host_interface_on_bus(
535                    path, prop, properties_iface, bus, interfaces)
536            except Exception:
537                continue
538            if info is not None:
539                prop, iface = info
540                return prop, iface, properties_iface
541
542    def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
543        for i in interfaces:
544            properties = self.try_properties_interface(iface.GetAll, i)
545            if not properties:
546                continue
547            match = obmc.utils.misc.find_case_insensitive(
548                prop, list(properties.keys()))
549            if match is None:
550                continue
551            prop = match
552            return prop, i
553
554
555class SchemaHandler(RouteHandler):
556    verbs = ['GET']
557    rules = '<path:path>/schema'
558    suppress_logging = True
559
560    def __init__(self, app, bus):
561        super(SchemaHandler, self).__init__(
562            app, bus, self.verbs, self.rules)
563
564    def find(self, path):
565        return self.try_mapper_call(
566            self.mapper.get_object,
567            path=path)
568
569    def setup(self, path):
570        request.route_data['map'] = self.find(path)
571
572    def do_get(self, path):
573        schema = {}
574        for x in request.route_data['map'].keys():
575            obj = self.bus.get_object(x, path, introspect=False)
576            iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
577            data = iface.Introspect()
578            parser = IntrospectionNodeParser(
579                ElementTree.fromstring(data))
580            for x, y in parser.get_interfaces().items():
581                schema[x] = y
582
583        return schema
584
585
586class InstanceHandler(RouteHandler):
587    verbs = ['GET', 'PUT', 'DELETE']
588    rules = '<path:path>'
589    request_type = dict
590
591    def __init__(self, app, bus):
592        super(InstanceHandler, self).__init__(
593            app, bus, self.verbs, self.rules)
594
595    def find(self, path, callback=None):
596        return {path: self.try_mapper_call(
597            self.mapper.get_object,
598            callback,
599            path=path)}
600
601    def setup(self, path):
602        callback = None
603        if request.method == 'PUT':
604            def callback(e, **kw):
605                abort(403, _4034_msg % ('resource', 'created', path))
606
607        if request.route_data.get('map') is None:
608            request.route_data['map'] = self.find(path, callback)
609
610    def do_get(self, path):
611        return self.mapper.enumerate_object(
612            path,
613            mapper_data=request.route_data['map'])
614
615    def do_put(self, path):
616        # make sure all properties exist in the request
617        obj = set(self.do_get(path).keys())
618        req = set(request.parameter_list.keys())
619
620        diff = list(obj.difference(req))
621        if diff:
622            abort(403, _4034_msg % (
623                'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
624
625        diff = list(req.difference(obj))
626        if diff:
627            abort(403, _4034_msg % (
628                'resource', 'created', '%s/attr/%s' % (path, diff[0])))
629
630        for p, v in request.parameter_list.items():
631            self.app.property_handler.do_put(
632                path, p, v)
633
634    def do_delete(self, path):
635        deleted = False
636        for bus, interfaces in request.route_data['map'][path].items():
637            if self.bus_has_delete(interfaces):
638                self.delete_on_bus(path, bus)
639                deleted = True
640
641        #It's OK if some objects didn't have a Delete, but not all
642        if not deleted:
643            abort(403, _4034_msg % ('resource', 'removed', path))
644
645    def bus_has_delete(self, interfaces):
646        return DELETE_IFACE in interfaces
647
648    def delete_on_bus(self, path, bus):
649        obj = self.bus.get_object(bus, path, introspect=False)
650        delete_iface = dbus.Interface(
651            obj, dbus_interface=DELETE_IFACE)
652        delete_iface.Delete()
653
654
655class SessionHandler(MethodHandler):
656    ''' Handles the /login and /logout routes, manages
657    server side session store and session cookies.  '''
658
659    rules = ['/login', '/logout']
660    login_str = "User '%s' logged %s"
661    bad_passwd_str = "Invalid username or password"
662    no_user_str = "No user logged in"
663    bad_json_str = "Expecting request format { 'data': " \
664        "[<username>, <password>] }, got '%s'"
665    bmc_not_ready_str = "BMC is not ready (booting)"
666    _require_auth = None
667    MAX_SESSIONS = 16
668    BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
669    BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
670    BMCSTATE_PROPERTY = 'CurrentBMCState'
671    BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
672    suppress_json_logging = True
673
674    def __init__(self, app, bus):
675        super(SessionHandler, self).__init__(
676            app, bus)
677        self.hmac_key = os.urandom(128)
678        self.session_store = []
679
680    @staticmethod
681    def authenticate(username, clear):
682        try:
683            encoded = spwd.getspnam(username)[1]
684            return encoded == crypt.crypt(clear, encoded)
685        except KeyError:
686            return False
687
688    def invalidate_session(self, session):
689        try:
690            self.session_store.remove(session)
691        except ValueError:
692            pass
693
694    def new_session(self):
695        sid = os.urandom(32)
696        if self.MAX_SESSIONS <= len(self.session_store):
697            self.session_store.pop()
698        self.session_store.insert(0, {'sid': sid})
699
700        return self.session_store[0]
701
702    def get_session(self, sid):
703        sids = [x['sid'] for x in self.session_store]
704        try:
705            return self.session_store[sids.index(sid)]
706        except ValueError:
707            return None
708
709    def get_session_from_cookie(self):
710        return self.get_session(
711            request.get_cookie(
712                'sid', secret=self.hmac_key))
713
714    def do_post(self, **kw):
715        if request.path == '/login':
716            return self.do_login(**kw)
717        else:
718            return self.do_logout(**kw)
719
720    def do_logout(self, **kw):
721        session = self.get_session_from_cookie()
722        if session is not None:
723            user = session['user']
724            self.invalidate_session(session)
725            response.delete_cookie('sid')
726            return self.login_str % (user, 'out')
727
728        return self.no_user_str
729
730    def do_login(self, **kw):
731        if len(request.parameter_list) != 2:
732            abort(400, self.bad_json_str % (request.json))
733
734        if not self.authenticate(*request.parameter_list):
735            abort(401, self.bad_passwd_str)
736
737        force = False
738        try:
739            force = request.json.get('force')
740        except (ValueError, AttributeError, KeyError, TypeError):
741            force = False
742
743        if not force and not self.is_bmc_ready():
744            abort(503, self.bmc_not_ready_str)
745
746        user = request.parameter_list[0]
747        session = self.new_session()
748        session['user'] = user
749        response.set_cookie(
750            'sid', session['sid'], secret=self.hmac_key,
751            secure=True,
752            httponly=True)
753        return self.login_str % (user, 'in')
754
755    def is_bmc_ready(self):
756        if not self.app.with_bmc_check:
757            return True
758
759        try:
760            obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
761            iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
762            state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
763            if state == self.BMCSTATE_READY:
764                return True
765
766        except dbus.exceptions.DBusException:
767            pass
768
769        return False
770
771    def find(self, **kw):
772        pass
773
774    def setup(self, **kw):
775        pass
776
777
778class ImageUploadUtils:
779    ''' Provides common utils for image upload. '''
780
781    file_loc = '/tmp/images'
782    file_prefix = 'img'
783    file_suffix = ''
784    signal = None
785
786    @classmethod
787    def do_upload(cls, filename=''):
788        def cleanup():
789            os.close(handle)
790            if cls.signal:
791                cls.signal.remove()
792                cls.signal = None
793
794        def signal_callback(path, a, **kw):
795            # Just interested on the first Version interface created which is
796            # triggered when the file is uploaded. This helps avoid getting the
797            # wrong information for multiple upload requests in a row.
798            if "xyz.openbmc_project.Software.Version" in a and \
799               "xyz.openbmc_project.Software.Activation" not in a:
800                paths.append(path)
801
802        while cls.signal:
803            # Serialize uploads by waiting for the signal to be cleared.
804            # This makes it easier to ensure that the version information
805            # is the right one instead of the data from another upload request.
806            gevent.sleep(1)
807        if not os.path.exists(cls.file_loc):
808            abort(500, "Error Directory not found")
809        paths = []
810        bus = dbus.SystemBus()
811        cls.signal = bus.add_signal_receiver(
812            signal_callback,
813            dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
814            signal_name='InterfacesAdded',
815            path=SOFTWARE_PATH)
816        if not filename:
817            handle, filename = tempfile.mkstemp(cls.file_suffix,
818                                                cls.file_prefix, cls.file_loc)
819        else:
820            filename = os.path.join(cls.file_loc, filename)
821            handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
822        try:
823            file_contents = request.body.read()
824            request.body.close()
825            os.write(handle, file_contents)
826            # Close file after writing, the image manager process watches for
827            # the close event to know the upload is complete.
828            os.close(handle)
829        except (IOError, ValueError) as e:
830            cleanup()
831            abort(400, str(e))
832        except Exception:
833            cleanup()
834            abort(400, "Unexpected Error")
835        loop = gobject.MainLoop()
836        gcontext = loop.get_context()
837        count = 0
838        version_id = ''
839        while loop is not None:
840            try:
841                if gcontext.pending():
842                    gcontext.iteration()
843                if not paths:
844                    gevent.sleep(1)
845                else:
846                    version_id = os.path.basename(paths.pop())
847                    break
848                count += 1
849                if count == 10:
850                    break
851            except Exception:
852                break
853        cls.signal.remove()
854        cls.signal = None
855        if version_id:
856            return version_id
857        else:
858            abort(400, "Version already exists or failed to be extracted")
859
860
861class ImagePostHandler(RouteHandler):
862    ''' Handles the /upload/image route. '''
863
864    verbs = ['POST']
865    rules = ['/upload/image']
866    content_type = 'application/octet-stream'
867
868    def __init__(self, app, bus):
869        super(ImagePostHandler, self).__init__(
870            app, bus, self.verbs, self.rules, self.content_type)
871
872    def do_post(self, filename=''):
873        return ImageUploadUtils.do_upload()
874
875    def find(self, **kw):
876        pass
877
878    def setup(self, **kw):
879        pass
880
881
882class CertificateHandler:
883    file_suffix = '.pem'
884    file_prefix = 'cert_'
885    CERT_PATH = '/xyz/openbmc_project/certs'
886    CERT_IFACE = 'xyz.openbmc_project.Certs.Install'
887
888    def __init__(self, route_handler, cert_type, service):
889        if not service:
890            abort(500, "Missing service")
891        if not cert_type:
892            abort(500, "Missing certificate type")
893        bus = dbus.SystemBus()
894        certPath = self.CERT_PATH + "/" + cert_type + "/" + service
895        intfs = route_handler.try_mapper_call(
896            route_handler.mapper.get_object, path=certPath)
897        for busName,intf in intfs.items():
898            if self.CERT_IFACE in intf:
899                self.obj = bus.get_object(busName, certPath)
900                return
901        abort(404, "Path not found")
902
903    def do_upload(self):
904        def cleanup():
905            if os.path.exists(temp.name):
906                os.remove(temp.name)
907
908        with tempfile.NamedTemporaryFile(
909            suffix=self.file_suffix,
910            prefix=self.file_prefix,
911            delete=False) as temp:
912            try:
913                file_contents = request.body.read()
914                request.body.close()
915                temp.write(file_contents)
916            except (IOError, ValueError) as e:
917                cleanup()
918                abort(500, str(e))
919            except Exception:
920                cleanup()
921                abort(500, "Unexpected Error")
922
923        try:
924            iface = dbus.Interface(self.obj, self.CERT_IFACE)
925            iface.Install(temp.name)
926        except Exception as e:
927            cleanup()
928            abort(400, str(e))
929        cleanup()
930
931    def do_delete(self):
932        delete_iface = dbus.Interface(
933            self.obj, dbus_interface=DELETE_IFACE)
934        delete_iface.Delete()
935
936
937class CertificatePutHandler(RouteHandler):
938    ''' Handles the /xyz/openbmc_project/certs/<cert_type>/<service> route. '''
939
940    verbs = ['PUT', 'DELETE']
941    rules = ['/xyz/openbmc_project/certs/<cert_type>/<service>']
942    content_type = 'application/octet-stream'
943
944    def __init__(self, app, bus):
945        super(CertificatePutHandler, self).__init__(
946            app, bus, self.verbs, self.rules, self.content_type)
947
948    def do_put(self, cert_type, service):
949        return CertificateHandler(self, cert_type, service).do_upload()
950
951    def do_delete(self, cert_type, service):
952        return CertificateHandler(self, cert_type, service).do_delete()
953
954    def find(self, **kw):
955        pass
956
957    def setup(self, **kw):
958        pass
959
960
961class EventNotifier:
962    keyNames = {}
963    keyNames['event'] = 'event'
964    keyNames['path'] = 'path'
965    keyNames['intfMap'] = 'interfaces'
966    keyNames['propMap'] = 'properties'
967    keyNames['intf'] = 'interface'
968
969    def __init__(self, wsock, filters):
970        self.wsock = wsock
971        self.paths = filters.get("paths", [])
972        self.interfaces = filters.get("interfaces", [])
973        self.signals = []
974        self.socket_error = False
975        if not self.paths:
976            self.paths.append(None)
977        bus = dbus.SystemBus()
978        # Add a signal receiver for every path the client is interested in
979        for path in self.paths:
980            add_sig = bus.add_signal_receiver(
981                self.interfaces_added_handler,
982                dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
983                signal_name='InterfacesAdded',
984                path=path)
985            chg_sig = bus.add_signal_receiver(
986                self.properties_changed_handler,
987                dbus_interface=dbus.PROPERTIES_IFACE,
988                signal_name='PropertiesChanged',
989                path=path,
990                path_keyword='path')
991            self.signals.append(add_sig)
992            self.signals.append(chg_sig)
993        loop = gobject.MainLoop()
994        # gobject's mainloop.run() will block the entire process, so the gevent
995        # scheduler and hence greenlets won't execute. The while-loop below
996        # works around this limitation by using gevent's sleep, instead of
997        # calling loop.run()
998        gcontext = loop.get_context()
999        while loop is not None:
1000            try:
1001                if self.socket_error:
1002                    for signal in self.signals:
1003                        signal.remove()
1004                    loop.quit()
1005                    break;
1006                if gcontext.pending():
1007                    gcontext.iteration()
1008                else:
1009                    # gevent.sleep puts only the current greenlet to sleep,
1010                    # not the entire process.
1011                    gevent.sleep(5)
1012            except WebSocketError:
1013                break
1014
1015    def interfaces_added_handler(self, path, iprops, **kw):
1016        ''' If the client is interested in these changes, respond to the
1017            client. This handles d-bus interface additions.'''
1018        if (not self.interfaces) or \
1019           (not set(iprops).isdisjoint(self.interfaces)):
1020            response = {}
1021            response[self.keyNames['event']] = "InterfacesAdded"
1022            response[self.keyNames['path']] = path
1023            response[self.keyNames['intfMap']] = iprops
1024            try:
1025                self.wsock.send(json.dumps(response))
1026            except:
1027                self.socket_error = True
1028                return
1029
1030    def properties_changed_handler(self, interface, new, old, **kw):
1031        ''' If the client is interested in these changes, respond to the
1032            client. This handles d-bus property changes. '''
1033        if (not self.interfaces) or (interface in self.interfaces):
1034            path = str(kw['path'])
1035            response = {}
1036            response[self.keyNames['event']] = "PropertiesChanged"
1037            response[self.keyNames['path']] = path
1038            response[self.keyNames['intf']] = interface
1039            response[self.keyNames['propMap']] = new
1040            try:
1041                self.wsock.send(json.dumps(response))
1042            except:
1043                self.socket_error = True
1044                return
1045
1046
1047class EventHandler(RouteHandler):
1048    ''' Handles the /subscribe route, for clients to be able
1049        to subscribe to BMC events. '''
1050
1051    verbs = ['GET']
1052    rules = ['/subscribe']
1053    suppress_logging = True
1054
1055    def __init__(self, app, bus):
1056        super(EventHandler, self).__init__(
1057            app, bus, self.verbs, self.rules)
1058
1059    def find(self, **kw):
1060        pass
1061
1062    def setup(self, **kw):
1063        pass
1064
1065    def do_get(self):
1066        wsock = request.environ.get('wsgi.websocket')
1067        if not wsock:
1068            abort(400, 'Expected WebSocket request.')
1069        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
1070        filters = wsock.receive()
1071        filters = json.loads(filters)
1072        notifier = EventNotifier(wsock, filters)
1073
1074class HostConsoleHandler(RouteHandler):
1075    ''' Handles the /console route, for clients to be able
1076        read/write the host serial console. The way this is
1077        done is by exposing a websocket that's mirrored to an
1078        abstract UNIX domain socket, which is the source for
1079        the console data. '''
1080
1081    verbs = ['GET']
1082    # Naming the route console0, because the numbering will help
1083    # on multi-bmc/multi-host systems.
1084    rules = ['/console0']
1085    suppress_logging = True
1086
1087    def __init__(self, app, bus):
1088        super(HostConsoleHandler, self).__init__(
1089            app, bus, self.verbs, self.rules)
1090
1091    def find(self, **kw):
1092        pass
1093
1094    def setup(self, **kw):
1095        pass
1096
1097    def read_wsock(self, wsock, sock):
1098        while True:
1099            try:
1100                incoming = wsock.receive()
1101                if incoming:
1102                    # Read websocket, write to UNIX socket
1103                    sock.send(incoming)
1104            except Exception as e:
1105                sock.close()
1106                return
1107
1108    def read_sock(self, sock, wsock):
1109        max_sock_read_len = 4096
1110        while True:
1111            try:
1112                outgoing = sock.recv(max_sock_read_len)
1113                if outgoing:
1114                    # Read UNIX socket, write to websocket
1115                    wsock.send(outgoing)
1116            except Exception as e:
1117                wsock.close()
1118                return
1119
1120    def do_get(self):
1121        wsock = request.environ.get('wsgi.websocket')
1122        if not wsock:
1123            abort(400, 'Expected WebSocket based request.')
1124
1125        # An abstract Unix socket path must be less than or equal to 108 bytes
1126        # and does not need to be nul-terminated or padded out to 108 bytes
1127        socket_name = "\0obmc-console"
1128        socket_path = socket_name
1129        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1130
1131        try:
1132            sock.connect(socket_path)
1133        except Exception as e:
1134            abort(500, str(e))
1135
1136        wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1137        sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
1138        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
1139        gevent.joinall([wsock_reader, sock_reader, ping_sender])
1140
1141
1142class ImagePutHandler(RouteHandler):
1143    ''' Handles the /upload/image/<filename> route. '''
1144
1145    verbs = ['PUT']
1146    rules = ['/upload/image/<filename>']
1147    content_type = 'application/octet-stream'
1148
1149    def __init__(self, app, bus):
1150        super(ImagePutHandler, self).__init__(
1151            app, bus, self.verbs, self.rules, self.content_type)
1152
1153    def do_put(self, filename=''):
1154        return ImageUploadUtils.do_upload(filename)
1155
1156    def find(self, **kw):
1157        pass
1158
1159    def setup(self, **kw):
1160        pass
1161
1162
1163class DownloadDumpHandler(RouteHandler):
1164    ''' Handles the /download/dump route. '''
1165
1166    verbs = 'GET'
1167    rules = ['/download/dump/<dumpid>']
1168    content_type = 'application/octet-stream'
1169    dump_loc = '/var/lib/phosphor-debug-collector/dumps'
1170    suppress_json_resp = True
1171    suppress_logging = True
1172
1173    def __init__(self, app, bus):
1174        super(DownloadDumpHandler, self).__init__(
1175            app, bus, self.verbs, self.rules, self.content_type)
1176
1177    def do_get(self, dumpid):
1178        return self.do_download(dumpid)
1179
1180    def find(self, **kw):
1181        pass
1182
1183    def setup(self, **kw):
1184        pass
1185
1186    def do_download(self, dumpid):
1187        dump_loc = os.path.join(self.dump_loc, dumpid)
1188        if not os.path.exists(dump_loc):
1189            abort(404, "Path not found")
1190
1191        files = os.listdir(dump_loc)
1192        num_files = len(files)
1193        if num_files == 0:
1194            abort(404, "Dump not found")
1195
1196        return static_file(os.path.basename(files[0]), root=dump_loc,
1197                           download=True, mimetype=self.content_type)
1198
1199
1200class WebHandler(RouteHandler):
1201    ''' Handles the routes for the web UI files. '''
1202
1203    verbs = 'GET'
1204
1205    # Match only what we know are web files, so everything else
1206    # can get routed to the REST handlers.
1207    rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1208             '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1209             '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1210             '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1211             '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1212             '/<filename:re:.+\.ico>']
1213
1214    # The mimetypes module knows about most types, but not these
1215    content_types = {
1216        '.eot': 'application/vnd.ms-fontobject',
1217        '.woff': 'application/x-font-woff',
1218        '.woff2': 'application/x-font-woff2',
1219        '.ttf': 'application/x-font-ttf',
1220        '.map': 'application/json'
1221    }
1222
1223    _require_auth = None
1224    suppress_json_resp = True
1225    suppress_logging = True
1226
1227    def __init__(self, app, bus):
1228        super(WebHandler, self).__init__(
1229            app, bus, self.verbs, self.rules)
1230
1231    def get_type(self, filename):
1232        ''' Returns the content type and encoding for a file '''
1233
1234        content_type, encoding = mimetypes.guess_type(filename)
1235
1236        # Try our own list if mimetypes didn't recognize it
1237        if content_type is None:
1238            if filename[-3:] == '.gz':
1239                filename = filename[:-3]
1240            extension = filename[filename.rfind('.'):]
1241            content_type = self.content_types.get(extension, None)
1242
1243        return content_type, encoding
1244
1245    def do_get(self, filename='index.html'):
1246
1247        # If a gzipped version exists, use that instead.
1248        # Possible future enhancement: if the client doesn't
1249        # accept compressed files, unzip it ourselves before sending.
1250        if not os.path.exists(os.path.join(www_base_path, filename)):
1251            filename = filename + '.gz'
1252
1253        # Though bottle should protect us, ensure path is valid
1254        realpath = os.path.realpath(filename)
1255        if realpath[0] == '/':
1256            realpath = realpath[1:]
1257        if not os.path.exists(os.path.join(www_base_path, realpath)):
1258            abort(404, "Path not found")
1259
1260        mimetype, encoding = self.get_type(filename)
1261
1262        # Couldn't find the type - let static_file() deal with it,
1263        # though this should never happen.
1264        if mimetype is None:
1265            print("Can't figure out content-type for %s" % filename)
1266            mimetype = 'auto'
1267
1268        # This call will set several header fields for us,
1269        # including the charset if the type is text.
1270        response = static_file(filename, www_base_path, mimetype)
1271
1272        # static_file() will only set the encoding if the
1273        # mimetype was auto, so set it here.
1274        if encoding is not None:
1275            response.set_header('Content-Encoding', encoding)
1276
1277        return response
1278
1279    def find(self, **kw):
1280        pass
1281
1282    def setup(self, **kw):
1283        pass
1284
1285
1286class AuthorizationPlugin(object):
1287    ''' Invokes an optional list of authorization callbacks. '''
1288
1289    name = 'authorization'
1290    api = 2
1291
1292    class Compose:
1293        def __init__(self, validators, callback, session_mgr):
1294            self.validators = validators
1295            self.callback = callback
1296            self.session_mgr = session_mgr
1297
1298        def __call__(self, *a, **kw):
1299            sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1300            session = self.session_mgr.get_session(sid)
1301            if request.method != 'OPTIONS':
1302                for x in self.validators:
1303                    x(session, *a, **kw)
1304
1305            return self.callback(*a, **kw)
1306
1307    def apply(self, callback, route):
1308        undecorated = route.get_undecorated_callback()
1309        if not isinstance(undecorated, RouteHandler):
1310            return callback
1311
1312        auth_types = getattr(
1313            undecorated, '_require_auth', None)
1314        if not auth_types:
1315            return callback
1316
1317        return self.Compose(
1318            auth_types, callback, undecorated.app.session_handler)
1319
1320
1321class CorsPlugin(object):
1322    ''' Add CORS headers. '''
1323
1324    name = 'cors'
1325    api = 2
1326
1327    @staticmethod
1328    def process_origin():
1329        origin = request.headers.get('Origin')
1330        if origin:
1331            response.add_header('Access-Control-Allow-Origin', origin)
1332            response.add_header(
1333                'Access-Control-Allow-Credentials', 'true')
1334
1335    @staticmethod
1336    def process_method_and_headers(verbs):
1337        method = request.headers.get('Access-Control-Request-Method')
1338        headers = request.headers.get('Access-Control-Request-Headers')
1339        if headers:
1340            headers = [x.lower() for x in headers.split(',')]
1341
1342        if method in verbs \
1343                and headers == ['content-type']:
1344            response.add_header('Access-Control-Allow-Methods', method)
1345            response.add_header(
1346                'Access-Control-Allow-Headers', 'Content-Type')
1347            response.add_header('X-Frame-Options', 'deny')
1348            response.add_header('X-Content-Type-Options', 'nosniff')
1349            response.add_header('X-XSS-Protection', '1; mode=block')
1350            response.add_header(
1351                'Content-Security-Policy', "default-src 'self'")
1352            response.add_header(
1353                'Strict-Transport-Security',
1354                'max-age=31536000; includeSubDomains; preload')
1355
1356    def __init__(self, app):
1357        app.install_error_callback(self.error_callback)
1358
1359    def apply(self, callback, route):
1360        undecorated = route.get_undecorated_callback()
1361        if not isinstance(undecorated, RouteHandler):
1362            return callback
1363
1364        if not getattr(undecorated, '_enable_cors', None):
1365            return callback
1366
1367        def wrap(*a, **kw):
1368            self.process_origin()
1369            self.process_method_and_headers(undecorated._verbs)
1370            return callback(*a, **kw)
1371
1372        return wrap
1373
1374    def error_callback(self, **kw):
1375        self.process_origin()
1376
1377
1378class JsonApiRequestPlugin(object):
1379    ''' Ensures request content satisfies the OpenBMC json api format. '''
1380    name = 'json_api_request'
1381    api = 2
1382
1383    error_str = "Expecting request format { 'data': <value> }, got '%s'"
1384    type_error_str = "Unsupported Content-Type: '%s'"
1385    json_type = "application/json"
1386    request_methods = ['PUT', 'POST', 'PATCH']
1387
1388    @staticmethod
1389    def content_expected():
1390        return request.method in JsonApiRequestPlugin.request_methods
1391
1392    def validate_request(self):
1393        if request.content_length > 0 and \
1394                request.content_type != self.json_type:
1395            abort(415, self.type_error_str % request.content_type)
1396
1397        try:
1398            request.parameter_list = request.json.get('data')
1399        except ValueError as e:
1400            abort(400, str(e))
1401        except (AttributeError, KeyError, TypeError):
1402            abort(400, self.error_str % request.json)
1403
1404    def apply(self, callback, route):
1405        content_type = getattr(
1406            route.get_undecorated_callback(), '_content_type', None)
1407        if self.json_type != content_type:
1408            return callback
1409
1410        verbs = getattr(
1411            route.get_undecorated_callback(), '_verbs', None)
1412        if verbs is None:
1413            return callback
1414
1415        if not set(self.request_methods).intersection(verbs):
1416            return callback
1417
1418        def wrap(*a, **kw):
1419            if self.content_expected():
1420                self.validate_request()
1421            return callback(*a, **kw)
1422
1423        return wrap
1424
1425
1426class JsonApiRequestTypePlugin(object):
1427    ''' Ensures request content type satisfies the OpenBMC json api format. '''
1428    name = 'json_api_method_request'
1429    api = 2
1430
1431    error_str = "Expecting request format { 'data': %s }, got '%s'"
1432    json_type = "application/json"
1433
1434    def apply(self, callback, route):
1435        content_type = getattr(
1436            route.get_undecorated_callback(), '_content_type', None)
1437        if self.json_type != content_type:
1438            return callback
1439
1440        request_type = getattr(
1441            route.get_undecorated_callback(), 'request_type', None)
1442        if request_type is None:
1443            return callback
1444
1445        def validate_request():
1446            if not isinstance(request.parameter_list, request_type):
1447                abort(400, self.error_str % (str(request_type), request.json))
1448
1449        def wrap(*a, **kw):
1450            if JsonApiRequestPlugin.content_expected():
1451                validate_request()
1452            return callback(*a, **kw)
1453
1454        return wrap
1455
1456
1457class JsonErrorsPlugin(JSONPlugin):
1458    ''' Extend the Bottle JSONPlugin such that it also encodes error
1459        responses. '''
1460
1461    def __init__(self, app, **kw):
1462        super(JsonErrorsPlugin, self).__init__(**kw)
1463        self.json_opts = {
1464            x: y for x, y in kw.items()
1465            if x in ['indent', 'sort_keys']}
1466        app.install_error_callback(self.error_callback)
1467
1468    def error_callback(self, response_object, response_body, **kw):
1469        response_body['body'] = json.dumps(response_object, **self.json_opts)
1470        response.content_type = 'application/json'
1471
1472
1473class JsonApiResponsePlugin(object):
1474    ''' Emits responses in the OpenBMC json api format. '''
1475    name = 'json_api_response'
1476    api = 2
1477
1478    @staticmethod
1479    def has_body():
1480        return request.method not in ['OPTIONS']
1481
1482    def __init__(self, app):
1483        app.install_error_callback(self.error_callback)
1484
1485    @staticmethod
1486    def dbus_boolean_to_bool(data):
1487        ''' Convert all dbus.Booleans to true/false instead of 1/0 as
1488            the JSON encoder thinks they're ints.  Note that unlike
1489            dicts and lists, tuples (from a dbus.Struct) are immutable
1490            so they need special handling. '''
1491
1492        def walkdict(data):
1493            for key, value in data.items():
1494                if isinstance(value, dbus.Boolean):
1495                    data[key] = bool(value)
1496                elif isinstance(value, tuple):
1497                    data[key] = walktuple(value)
1498                else:
1499                    JsonApiResponsePlugin.dbus_boolean_to_bool(value)
1500
1501        def walklist(data):
1502            for i in range(len(data)):
1503                if isinstance(data[i], dbus.Boolean):
1504                    data[i] = bool(data[i])
1505                elif isinstance(data[i], tuple):
1506                    data[i] = walktuple(data[i])
1507                else:
1508                    JsonApiResponsePlugin.dbus_boolean_to_bool(data[i])
1509
1510        def walktuple(data):
1511            new = []
1512            for item in data:
1513                if isinstance(item, dbus.Boolean):
1514                    item = bool(item)
1515                else:
1516                    JsonApiResponsePlugin.dbus_boolean_to_bool(item)
1517                new.append(item)
1518            return tuple(new)
1519
1520        if isinstance(data, dict):
1521            walkdict(data)
1522        elif isinstance(data, list):
1523            walklist(data)
1524
1525    def apply(self, callback, route):
1526        skip = getattr(
1527            route.get_undecorated_callback(), 'suppress_json_resp', None)
1528        if skip:
1529            return callback
1530
1531        def wrap(*a, **kw):
1532            data = callback(*a, **kw)
1533            JsonApiResponsePlugin.dbus_boolean_to_bool(data)
1534            if self.has_body():
1535                resp = {'data': data}
1536                resp['status'] = 'ok'
1537                resp['message'] = response.status_line
1538                return resp
1539        return wrap
1540
1541    def error_callback(self, error, response_object, **kw):
1542        response_object['message'] = error.status_line
1543        response_object['status'] = 'error'
1544        response_object.setdefault('data', {})['description'] = str(error.body)
1545        if error.status_code == 500:
1546            response_object['data']['exception'] = repr(error.exception)
1547            response_object['data']['traceback'] = error.traceback.splitlines()
1548
1549
1550class JsonpPlugin(object):
1551    ''' Json javascript wrapper. '''
1552    name = 'jsonp'
1553    api = 2
1554
1555    def __init__(self, app, **kw):
1556        app.install_error_callback(self.error_callback)
1557
1558    @staticmethod
1559    def to_jsonp(json):
1560        jwrapper = request.query.callback or None
1561        if(jwrapper):
1562            response.set_header('Content-Type', 'application/javascript')
1563            json = jwrapper + '(' + json + ');'
1564        return json
1565
1566    def apply(self, callback, route):
1567        def wrap(*a, **kw):
1568            return self.to_jsonp(callback(*a, **kw))
1569        return wrap
1570
1571    def error_callback(self, response_body, **kw):
1572        response_body['body'] = self.to_jsonp(response_body['body'])
1573
1574
1575class ContentCheckerPlugin(object):
1576    ''' Ensures that a route is associated with the expected content-type
1577        header. '''
1578    name = 'content_checker'
1579    api = 2
1580
1581    class Checker:
1582        def __init__(self, type, callback):
1583            self.expected_type = type
1584            self.callback = callback
1585            self.error_str = "Expecting content type '%s', got '%s'"
1586
1587        def __call__(self, *a, **kw):
1588            if request.method in ['PUT', 'POST', 'PATCH'] and \
1589                    self.expected_type and \
1590                    self.expected_type != request.content_type:
1591                abort(415, self.error_str % (self.expected_type,
1592                      request.content_type))
1593
1594            return self.callback(*a, **kw)
1595
1596    def apply(self, callback, route):
1597        content_type = getattr(
1598            route.get_undecorated_callback(), '_content_type', None)
1599
1600        return self.Checker(content_type, callback)
1601
1602
1603class LoggingPlugin(object):
1604    ''' Wraps a request in order to emit a log after the request is handled. '''
1605    name = 'loggingp'
1606    api = 2
1607
1608    class Logger:
1609        def __init__(self, suppress_json_logging, callback, app):
1610            self.suppress_json_logging = suppress_json_logging
1611            self.callback = callback
1612            self.app = app
1613            self.logging_enabled = None
1614            self.bus = dbus.SystemBus()
1615            self.dbus_path = '/xyz/openbmc_project/logging/rest_api_logs'
1616            self.no_json = [
1617                '/xyz/openbmc_project/user/ldap/action/CreateConfig'
1618            ]
1619            self.bus.add_signal_receiver(
1620                self.properties_changed_handler,
1621                dbus_interface=dbus.PROPERTIES_IFACE,
1622                signal_name='PropertiesChanged',
1623                path=self.dbus_path)
1624            Greenlet.spawn(self.dbus_loop)
1625
1626        def __call__(self, *a, **kw):
1627            resp = self.callback(*a, **kw)
1628            if not self.enabled():
1629                return resp
1630            if request.method == 'GET':
1631                return resp
1632            json = request.json
1633            if self.suppress_json_logging:
1634                json = None
1635            elif any(substring in request.url for substring in self.no_json):
1636                json = None
1637            session = self.app.session_handler.get_session_from_cookie()
1638            user = None
1639            if "/login" in request.url:
1640                user = request.parameter_list[0]
1641            elif session is not None:
1642                user = session['user']
1643            print("{remote} user:{user} {method} {url} json:{json} {status}" \
1644                .format(
1645                    user=user,
1646                    remote=request.remote_addr,
1647                    method=request.method,
1648                    url=request.url,
1649                    json=json,
1650                    status=response.status))
1651            return resp
1652
1653        def enabled(self):
1654            if self.logging_enabled is None:
1655                try:
1656                    obj = self.bus.get_object(
1657                              'xyz.openbmc_project.Settings',
1658                              self.dbus_path)
1659                    iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
1660                    logging_enabled = iface.Get(
1661                                          'xyz.openbmc_project.Object.Enable',
1662                                          'Enabled')
1663                    self.logging_enabled = logging_enabled
1664                except dbus.exceptions.DBusException:
1665                    self.logging_enabled = False
1666            return self.logging_enabled
1667
1668        def dbus_loop(self):
1669            loop = gobject.MainLoop()
1670            gcontext = loop.get_context()
1671            while loop is not None:
1672                try:
1673                    if gcontext.pending():
1674                        gcontext.iteration()
1675                    else:
1676                        gevent.sleep(5)
1677                except Exception as e:
1678                    break
1679
1680        def properties_changed_handler(self, interface, new, old, **kw):
1681            self.logging_enabled = new.values()[0]
1682
1683    def apply(self, callback, route):
1684        cb = route.get_undecorated_callback()
1685        skip = getattr(
1686            cb, 'suppress_logging', None)
1687        if skip:
1688            return callback
1689
1690        suppress_json_logging = getattr(
1691            cb, 'suppress_json_logging', None)
1692        return self.Logger(suppress_json_logging, callback, cb.app)
1693
1694
1695class App(Bottle):
1696    def __init__(self, **kw):
1697        super(App, self).__init__(autojson=False)
1698
1699        self.have_wsock = kw.get('have_wsock', False)
1700        self.with_bmc_check = '--with-bmc-check' in sys.argv
1701
1702        self.bus = dbus.SystemBus()
1703        self.mapper = obmc.mapper.Mapper(self.bus)
1704        self.error_callbacks = []
1705
1706        self.install_hooks()
1707        self.install_plugins()
1708        self.create_handlers()
1709        self.install_handlers()
1710
1711    def install_plugins(self):
1712        # install json api plugins
1713        json_kw = {'indent': 2, 'sort_keys': True}
1714        self.install(AuthorizationPlugin())
1715        self.install(CorsPlugin(self))
1716        self.install(ContentCheckerPlugin())
1717        self.install(JsonpPlugin(self, **json_kw))
1718        self.install(JsonErrorsPlugin(self, **json_kw))
1719        self.install(JsonApiResponsePlugin(self))
1720        self.install(JsonApiRequestPlugin())
1721        self.install(JsonApiRequestTypePlugin())
1722        self.install(LoggingPlugin())
1723
1724    def install_hooks(self):
1725        self.error_handler_type = type(self.default_error_handler)
1726        self.original_error_handler = self.default_error_handler
1727        self.default_error_handler = self.error_handler_type(
1728            self.custom_error_handler, self, Bottle)
1729
1730        self.real_router_match = self.router.match
1731        self.router.match = self.custom_router_match
1732        self.add_hook('before_request', self.strip_extra_slashes)
1733
1734    def create_handlers(self):
1735        # create route handlers
1736        self.session_handler = SessionHandler(self, self.bus)
1737        self.web_handler = WebHandler(self, self.bus)
1738        self.directory_handler = DirectoryHandler(self, self.bus)
1739        self.list_names_handler = ListNamesHandler(self, self.bus)
1740        self.list_handler = ListHandler(self, self.bus)
1741        self.method_handler = MethodHandler(self, self.bus)
1742        self.property_handler = PropertyHandler(self, self.bus)
1743        self.schema_handler = SchemaHandler(self, self.bus)
1744        self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1745        self.image_upload_put_handler = ImagePutHandler(self, self.bus)
1746        self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
1747        self.certificate_put_handler = CertificatePutHandler(self, self.bus)
1748        if self.have_wsock:
1749            self.event_handler = EventHandler(self, self.bus)
1750            self.host_console_handler = HostConsoleHandler(self, self.bus)
1751        self.instance_handler = InstanceHandler(self, self.bus)
1752
1753    def install_handlers(self):
1754        self.session_handler.install()
1755        self.web_handler.install()
1756        self.directory_handler.install()
1757        self.list_names_handler.install()
1758        self.list_handler.install()
1759        self.method_handler.install()
1760        self.property_handler.install()
1761        self.schema_handler.install()
1762        self.image_upload_post_handler.install()
1763        self.image_upload_put_handler.install()
1764        self.download_dump_get_handler.install()
1765        self.certificate_put_handler.install()
1766        if self.have_wsock:
1767            self.event_handler.install()
1768            self.host_console_handler.install()
1769        # this has to come last, since it matches everything
1770        self.instance_handler.install()
1771
1772    def install_error_callback(self, callback):
1773        self.error_callbacks.insert(0, callback)
1774
1775    def custom_router_match(self, environ):
1776        ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1777            needed doesn't work for us since the instance rules match
1778            everything. This monkey-patch lets the route handler figure
1779            out which response is needed.  This could be accomplished
1780            with a hook but that would require calling the router match
1781            function twice.
1782        '''
1783        route, args = self.real_router_match(environ)
1784        if isinstance(route.callback, RouteHandler):
1785            route.callback._setup(**args)
1786
1787        return route, args
1788
1789    def custom_error_handler(self, res, error):
1790        ''' Allow plugins to modify error responses too via this custom
1791            error handler. '''
1792
1793        response_object = {}
1794        response_body = {}
1795        for x in self.error_callbacks:
1796            x(error=error,
1797                response_object=response_object,
1798                response_body=response_body)
1799
1800        return response_body.get('body', "")
1801
1802    @staticmethod
1803    def strip_extra_slashes():
1804        path = request.environ['PATH_INFO']
1805        trailing = ("", "/")[path[-1] == '/']
1806        parts = list(filter(bool, path.split('/')))
1807        request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
1808