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