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