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
34import fnmatch
35have_wsock = True
36try:
37    from geventwebsocket import WebSocketError
38except ImportError:
39    have_wsock = False
40if have_wsock:
41    from dbus.mainloop.glib import DBusGMainLoop
42    DBusGMainLoop(set_as_default=True)
43    # TODO: openbmc/openbmc#2994 remove python 2 support
44    try:  # python 2
45        import gobject
46    except ImportError:  # python 3
47        from gi.repository import GObject as gobject
48    import gevent
49    from gevent import socket
50    from gevent import Greenlet
51
52DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
53DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
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
294    def __init__(self, app, bus):
295        super(DirectoryHandler, self).__init__(
296            app, bus, self.verbs, self.rules)
297
298    def find(self, path='/'):
299        return self.try_mapper_call(
300            self.mapper.get_subtree_paths, path=path, depth=1)
301
302    def setup(self, path='/'):
303        request.route_data['map'] = self.find(path)
304
305    def do_get(self, path='/'):
306        return request.route_data['map']
307
308
309class ListNamesHandler(RouteHandler):
310    verbs = 'GET'
311    rules = ['/list', '<path:path>/list']
312
313    def __init__(self, app, bus):
314        super(ListNamesHandler, self).__init__(
315            app, bus, self.verbs, self.rules)
316
317    def find(self, path='/'):
318        return list(self.try_mapper_call(
319            self.mapper.get_subtree, path=path).keys())
320
321    def setup(self, path='/'):
322        request.route_data['map'] = self.find(path)
323
324    def do_get(self, path='/'):
325        return request.route_data['map']
326
327
328class ListHandler(RouteHandler):
329    verbs = 'GET'
330    rules = ['/enumerate', '<path:path>/enumerate']
331
332    def __init__(self, app, bus):
333        super(ListHandler, self).__init__(
334            app, bus, self.verbs, self.rules)
335
336    def find(self, path='/'):
337        return self.try_mapper_call(
338            self.mapper.get_subtree, path=path)
339
340    def setup(self, path='/'):
341        request.route_data['map'] = self.find(path)
342
343    def do_get(self, path='/'):
344        return {x: y for x, y in self.mapper.enumerate_subtree(
345                path,
346                mapper_data=request.route_data['map']).dataitems()}
347
348
349class MethodHandler(RouteHandler):
350    verbs = 'POST'
351    rules = '<path:path>/action/<method>'
352    request_type = list
353    content_type = 'application/json'
354
355    def __init__(self, app, bus):
356        super(MethodHandler, self).__init__(
357            app, bus, self.verbs, self.rules, self.content_type)
358        self.service = ''
359        self.interface = ''
360
361    def find(self, path, method):
362        method_list = []
363        buses = self.try_mapper_call(
364            self.mapper.get_object, path=path)
365        for items in buses.items():
366            m = self.find_method_on_bus(path, method, *items)
367            if m:
368                method_list.append(m)
369        if method_list:
370            return method_list
371
372        abort(404, _4034_msg % ('method', 'found', method))
373
374    def setup(self, path, method):
375        request.route_data['map'] = self.find(path, method)
376
377    def do_post(self, path, method, retry=True):
378        try:
379            args = []
380            if request.parameter_list:
381                args = request.parameter_list
382            # To see if the return type is capable of being merged
383            if len(request.route_data['map']) > 1:
384                results = None
385                for item in request.route_data['map']:
386                    tmp = item(*args)
387                    if not results:
388                        if tmp is not None:
389                            results = type(tmp)()
390                    if isinstance(results, dict):
391                        results = results.update(tmp)
392                    elif isinstance(results, list):
393                        results = results + tmp
394                    elif isinstance(results, type(None)):
395                        results = None
396                    else:
397                        abort(501, 'Don\'t know how to merge method call '
398                                   'results of {}'.format(type(tmp)))
399                return results
400            # There is only one method
401            return request.route_data['map'][0](*args)
402
403        except dbus.exceptions.DBusException as e:
404            paramlist = []
405            if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
406
407                signature_list = get_method_signature(self.bus, self.service,
408                                                      path, self.interface,
409                                                      method)
410                if not signature_list:
411                    abort(400, "Failed to get method signature: %s" % str(e))
412                if len(signature_list) != len(request.parameter_list):
413                    abort(400, "Invalid number of args")
414                converted_value = None
415                try:
416                    for index, expected_type in enumerate(signature_list):
417                        value = request.parameter_list[index]
418                        converted_value = convert_type(expected_type, value)
419                        paramlist.append(converted_value)
420                    request.parameter_list = paramlist
421                    self.do_post(path, method, False)
422                    return
423                except Exception as ex:
424                    abort(400, "Bad Request/Invalid Args given")
425                abort(400, str(e))
426
427            if e.get_dbus_name() == DBUS_TYPE_ERROR:
428                abort(400, str(e))
429            raise
430
431    @staticmethod
432    def find_method_in_interface(method, obj, interface, methods):
433        if methods is None:
434            return None
435
436        method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
437        if method is not None:
438            iface = dbus.Interface(obj, interface)
439            return iface.get_dbus_method(method)
440
441    def find_method_on_bus(self, path, method, bus, interfaces):
442        obj = self.bus.get_object(bus, path, introspect=False)
443        iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
444        data = iface.Introspect()
445        parser = IntrospectionNodeParser(
446            ElementTree.fromstring(data),
447            intf_match=lambda x: x in interfaces)
448        for x, y in parser.get_interfaces().items():
449            m = self.find_method_in_interface(
450                method, obj, x, y.get('method'))
451            if m:
452                self.service = bus
453                self.interface = x
454                return m
455
456
457class PropertyHandler(RouteHandler):
458    verbs = ['PUT', 'GET']
459    rules = '<path:path>/attr/<prop>'
460    content_type = 'application/json'
461
462    def __init__(self, app, bus):
463        super(PropertyHandler, self).__init__(
464            app, bus, self.verbs, self.rules, self.content_type)
465
466    def find(self, path, prop):
467        self.app.instance_handler.setup(path)
468        obj = self.app.instance_handler.do_get(path)
469        real_name = obmc.utils.misc.find_case_insensitive(
470            prop, list(obj.keys()))
471
472        if not real_name:
473            if request.method == 'PUT':
474                abort(403, _4034_msg % ('property', 'created', prop))
475            else:
476                abort(404, _4034_msg % ('property', 'found', prop))
477        return real_name, {path: obj}
478
479    def setup(self, path, prop):
480        name, obj = self.find(path, prop)
481        request.route_data['obj'] = obj
482        request.route_data['name'] = name
483
484    def do_get(self, path, prop):
485        name = request.route_data['name']
486        return request.route_data['obj'][path][name]
487
488    def do_put(self, path, prop, value=None, retry=True):
489        if value is None:
490            value = request.parameter_list
491
492        prop, iface, properties_iface = self.get_host_interface(
493            path, prop, request.route_data['map'][path])
494        try:
495            properties_iface.Set(iface, prop, value)
496        except ValueError as e:
497            abort(400, str(e))
498        except dbus.exceptions.DBusException as e:
499            if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
500                bus_name = properties_iface.bus_name
501                expected_type = get_type_signature_by_introspection(self.bus,
502                                                                    bus_name,
503                                                                    path,
504                                                                    prop)
505                if not expected_type:
506                    abort(403, "Failed to get expected type: %s" % str(e))
507                converted_value = None
508                try:
509                    converted_value = convert_type(expected_type, value)
510                except Exception as ex:
511                    abort(403, "Failed to convert %s to type %s" %
512                          (value, expected_type))
513                try:
514                    self.do_put(path, prop, converted_value, False)
515                    return
516                except Exception as ex:
517                    abort(403, str(ex))
518
519                abort(403, str(e))
520            raise
521
522    def get_host_interface(self, path, prop, bus_info):
523        for bus, interfaces in bus_info.items():
524            obj = self.bus.get_object(bus, path, introspect=True)
525            properties_iface = dbus.Interface(
526                obj, dbus_interface=dbus.PROPERTIES_IFACE)
527
528            info = self.get_host_interface_on_bus(
529                path, prop, properties_iface, bus, interfaces)
530            if info is not None:
531                prop, iface = info
532                return prop, iface, properties_iface
533
534    def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
535        for i in interfaces:
536            properties = self.try_properties_interface(iface.GetAll, i)
537            if not properties:
538                continue
539            match = obmc.utils.misc.find_case_insensitive(
540                prop, list(properties.keys()))
541            if match is None:
542                continue
543            prop = match
544            return prop, i
545
546
547class SchemaHandler(RouteHandler):
548    verbs = ['GET']
549    rules = '<path:path>/schema'
550
551    def __init__(self, app, bus):
552        super(SchemaHandler, self).__init__(
553            app, bus, self.verbs, self.rules)
554
555    def find(self, path):
556        return self.try_mapper_call(
557            self.mapper.get_object,
558            path=path)
559
560    def setup(self, path):
561        request.route_data['map'] = self.find(path)
562
563    def do_get(self, path):
564        schema = {}
565        for x in request.route_data['map'].keys():
566            obj = self.bus.get_object(x, path, introspect=False)
567            iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
568            data = iface.Introspect()
569            parser = IntrospectionNodeParser(
570                ElementTree.fromstring(data))
571            for x, y in parser.get_interfaces().items():
572                schema[x] = y
573
574        return schema
575
576
577class InstanceHandler(RouteHandler):
578    verbs = ['GET', 'PUT', 'DELETE']
579    rules = '<path:path>'
580    request_type = dict
581
582    def __init__(self, app, bus):
583        super(InstanceHandler, self).__init__(
584            app, bus, self.verbs, self.rules)
585
586    def find(self, path, callback=None):
587        return {path: self.try_mapper_call(
588            self.mapper.get_object,
589            callback,
590            path=path)}
591
592    def setup(self, path):
593        callback = None
594        if request.method == 'PUT':
595            def callback(e, **kw):
596                abort(403, _4034_msg % ('resource', 'created', path))
597
598        if request.route_data.get('map') is None:
599            request.route_data['map'] = self.find(path, callback)
600
601    def do_get(self, path):
602        return self.mapper.enumerate_object(
603            path,
604            mapper_data=request.route_data['map'])
605
606    def do_put(self, path):
607        # make sure all properties exist in the request
608        obj = set(self.do_get(path).keys())
609        req = set(request.parameter_list.keys())
610
611        diff = list(obj.difference(req))
612        if diff:
613            abort(403, _4034_msg % (
614                'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
615
616        diff = list(req.difference(obj))
617        if diff:
618            abort(403, _4034_msg % (
619                'resource', 'created', '%s/attr/%s' % (path, diff[0])))
620
621        for p, v in request.parameter_list.items():
622            self.app.property_handler.do_put(
623                path, p, v)
624
625    def do_delete(self, path):
626        deleted = False
627        for bus, interfaces in request.route_data['map'][path].items():
628            if self.bus_has_delete(interfaces):
629                self.delete_on_bus(path, bus)
630                deleted = True
631
632        #It's OK if some objects didn't have a Delete, but not all
633        if not deleted:
634            abort(403, _4034_msg % ('resource', 'removed', path))
635
636    def bus_has_delete(self, interfaces):
637        return DELETE_IFACE in interfaces
638
639    def delete_on_bus(self, path, bus):
640        obj = self.bus.get_object(bus, path, introspect=False)
641        delete_iface = dbus.Interface(
642            obj, dbus_interface=DELETE_IFACE)
643        delete_iface.Delete()
644
645
646class SessionHandler(MethodHandler):
647    ''' Handles the /login and /logout routes, manages
648    server side session store and session cookies.  '''
649
650    rules = ['/login', '/logout']
651    login_str = "User '%s' logged %s"
652    bad_passwd_str = "Invalid username or password"
653    no_user_str = "No user logged in"
654    bad_json_str = "Expecting request format { 'data': " \
655        "[<username>, <password>] }, got '%s'"
656    bmc_not_ready_str = "BMC is not ready (booting)"
657    _require_auth = None
658    MAX_SESSIONS = 16
659    BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
660    BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
661    BMCSTATE_PROPERTY = 'CurrentBMCState'
662    BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
663
664    def __init__(self, app, bus):
665        super(SessionHandler, self).__init__(
666            app, bus)
667        self.hmac_key = os.urandom(128)
668        self.session_store = []
669
670    @staticmethod
671    def authenticate(username, clear):
672        try:
673            encoded = spwd.getspnam(username)[1]
674            return encoded == crypt.crypt(clear, encoded)
675        except KeyError:
676            return False
677
678    def invalidate_session(self, session):
679        try:
680            self.session_store.remove(session)
681        except ValueError:
682            pass
683
684    def new_session(self):
685        sid = os.urandom(32)
686        if self.MAX_SESSIONS <= len(self.session_store):
687            self.session_store.pop()
688        self.session_store.insert(0, {'sid': sid})
689
690        return self.session_store[0]
691
692    def get_session(self, sid):
693        sids = [x['sid'] for x in self.session_store]
694        try:
695            return self.session_store[sids.index(sid)]
696        except ValueError:
697            return None
698
699    def get_session_from_cookie(self):
700        return self.get_session(
701            request.get_cookie(
702                'sid', secret=self.hmac_key))
703
704    def do_post(self, **kw):
705        if request.path == '/login':
706            return self.do_login(**kw)
707        else:
708            return self.do_logout(**kw)
709
710    def do_logout(self, **kw):
711        session = self.get_session_from_cookie()
712        if session is not None:
713            user = session['user']
714            self.invalidate_session(session)
715            response.delete_cookie('sid')
716            return self.login_str % (user, 'out')
717
718        return self.no_user_str
719
720    def do_login(self, **kw):
721        if len(request.parameter_list) != 2:
722            abort(400, self.bad_json_str % (request.json))
723
724        if not self.authenticate(*request.parameter_list):
725            abort(401, self.bad_passwd_str)
726
727        force = False
728        try:
729            force = request.json.get('force')
730        except (ValueError, AttributeError, KeyError, TypeError):
731            force = False
732
733        if not force and not self.is_bmc_ready():
734            abort(503, self.bmc_not_ready_str)
735
736        user = request.parameter_list[0]
737        session = self.new_session()
738        session['user'] = user
739        response.set_cookie(
740            'sid', session['sid'], secret=self.hmac_key,
741            secure=True,
742            httponly=True)
743        return self.login_str % (user, 'in')
744
745    def is_bmc_ready(self):
746        if not self.app.with_bmc_check:
747            return True
748
749        try:
750            obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
751            iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
752            state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
753            if state == self.BMCSTATE_READY:
754                return True
755
756        except dbus.exceptions.DBusException:
757            pass
758
759        return False
760
761    def find(self, **kw):
762        pass
763
764    def setup(self, **kw):
765        pass
766
767
768class ImageUploadUtils:
769    ''' Provides common utils for image upload. '''
770
771    file_loc = '/tmp/images'
772    file_prefix = 'img'
773    file_suffix = ''
774    signal = None
775
776    @classmethod
777    def do_upload(cls, filename=''):
778        def cleanup():
779            os.close(handle)
780            if cls.signal:
781                cls.signal.remove()
782                cls.signal = None
783
784        def signal_callback(path, a, **kw):
785            # Just interested on the first Version interface created which is
786            # triggered when the file is uploaded. This helps avoid getting the
787            # wrong information for multiple upload requests in a row.
788            if "xyz.openbmc_project.Software.Version" in a and \
789               "xyz.openbmc_project.Software.Activation" not in a:
790                paths.append(path)
791
792        while cls.signal:
793            # Serialize uploads by waiting for the signal to be cleared.
794            # This makes it easier to ensure that the version information
795            # is the right one instead of the data from another upload request.
796            gevent.sleep(1)
797        if not os.path.exists(cls.file_loc):
798            abort(500, "Error Directory not found")
799        paths = []
800        bus = dbus.SystemBus()
801        cls.signal = bus.add_signal_receiver(
802            signal_callback,
803            dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
804            signal_name='InterfacesAdded',
805            path=SOFTWARE_PATH)
806        if not filename:
807            handle, filename = tempfile.mkstemp(cls.file_suffix,
808                                                cls.file_prefix, cls.file_loc)
809        else:
810            filename = os.path.join(cls.file_loc, filename)
811            handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
812        try:
813            file_contents = request.body.read()
814            request.body.close()
815            os.write(handle, file_contents)
816            # Close file after writing, the image manager process watches for
817            # the close event to know the upload is complete.
818            os.close(handle)
819        except (IOError, ValueError) as e:
820            cleanup()
821            abort(400, str(e))
822        except Exception:
823            cleanup()
824            abort(400, "Unexpected Error")
825        loop = gobject.MainLoop()
826        gcontext = loop.get_context()
827        count = 0
828        version_id = ''
829        while loop is not None:
830            try:
831                if gcontext.pending():
832                    gcontext.iteration()
833                if not paths:
834                    gevent.sleep(1)
835                else:
836                    version_id = os.path.basename(paths.pop())
837                    break
838                count += 1
839                if count == 10:
840                    break
841            except Exception:
842                break
843        cls.signal.remove()
844        cls.signal = None
845        if version_id:
846            return version_id
847        else:
848            abort(400, "Version already exists or failed to be extracted")
849
850
851class ImagePostHandler(RouteHandler):
852    ''' Handles the /upload/image route. '''
853
854    verbs = ['POST']
855    rules = ['/upload/image']
856    content_type = 'application/octet-stream'
857
858    def __init__(self, app, bus):
859        super(ImagePostHandler, self).__init__(
860            app, bus, self.verbs, self.rules, self.content_type)
861
862    def do_post(self, filename=''):
863        return ImageUploadUtils.do_upload()
864
865    def find(self, **kw):
866        pass
867
868    def setup(self, **kw):
869        pass
870
871
872class EventNotifier:
873    keyNames = {}
874    keyNames['event'] = 'event'
875    keyNames['path'] = 'path'
876    keyNames['intfMap'] = 'interfaces'
877    keyNames['propMap'] = 'properties'
878    keyNames['intf'] = 'interface'
879
880    def __init__(self, wsock, filters):
881        self.wsock = wsock
882        self.paths = filters.get("paths", [])
883        self.interfaces = filters.get("interfaces", [])
884        if not self.paths:
885            self.paths.append(None)
886        bus = dbus.SystemBus()
887        # Add a signal receiver for every path the client is interested in
888        for path in self.paths:
889            bus.add_signal_receiver(
890                self.interfaces_added_handler,
891                dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
892                signal_name='InterfacesAdded',
893                path=path)
894            bus.add_signal_receiver(
895                self.properties_changed_handler,
896                dbus_interface=dbus.PROPERTIES_IFACE,
897                signal_name='PropertiesChanged',
898                path=path,
899                path_keyword='path')
900        loop = gobject.MainLoop()
901        # gobject's mainloop.run() will block the entire process, so the gevent
902        # scheduler and hence greenlets won't execute. The while-loop below
903        # works around this limitation by using gevent's sleep, instead of
904        # calling loop.run()
905        gcontext = loop.get_context()
906        while loop is not None:
907            try:
908                if gcontext.pending():
909                    gcontext.iteration()
910                else:
911                    # gevent.sleep puts only the current greenlet to sleep,
912                    # not the entire process.
913                    gevent.sleep(5)
914            except WebSocketError:
915                break
916
917    def interfaces_added_handler(self, path, iprops, **kw):
918        ''' If the client is interested in these changes, respond to the
919            client. This handles d-bus interface additions.'''
920        if (not self.interfaces) or \
921           (not set(iprops).isdisjoint(self.interfaces)):
922            response = {}
923            response[self.keyNames['event']] = "InterfacesAdded"
924            response[self.keyNames['path']] = path
925            response[self.keyNames['intfMap']] = iprops
926            try:
927                self.wsock.send(json.dumps(response))
928            except WebSocketError:
929                return
930
931    def properties_changed_handler(self, interface, new, old, **kw):
932        ''' If the client is interested in these changes, respond to the
933            client. This handles d-bus property changes. '''
934        if (not self.interfaces) or (interface in self.interfaces):
935            path = str(kw['path'])
936            response = {}
937            response[self.keyNames['event']] = "PropertiesChanged"
938            response[self.keyNames['path']] = path
939            response[self.keyNames['intf']] = interface
940            response[self.keyNames['propMap']] = new
941            try:
942                self.wsock.send(json.dumps(response))
943            except WebSocketError:
944                return
945
946
947class EventHandler(RouteHandler):
948    ''' Handles the /subscribe route, for clients to be able
949        to subscribe to BMC events. '''
950
951    verbs = ['GET']
952    rules = ['/subscribe']
953
954    def __init__(self, app, bus):
955        super(EventHandler, self).__init__(
956            app, bus, self.verbs, self.rules)
957
958    def find(self, **kw):
959        pass
960
961    def setup(self, **kw):
962        pass
963
964    def do_get(self):
965        wsock = request.environ.get('wsgi.websocket')
966        if not wsock:
967            abort(400, 'Expected WebSocket request.')
968        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
969        filters = wsock.receive()
970        filters = json.loads(filters)
971        notifier = EventNotifier(wsock, filters)
972
973class HostConsoleHandler(RouteHandler):
974    ''' Handles the /console route, for clients to be able
975        read/write the host serial console. The way this is
976        done is by exposing a websocket that's mirrored to an
977        abstract UNIX domain socket, which is the source for
978        the console data. '''
979
980    verbs = ['GET']
981    # Naming the route console0, because the numbering will help
982    # on multi-bmc/multi-host systems.
983    rules = ['/console0']
984
985    def __init__(self, app, bus):
986        super(HostConsoleHandler, self).__init__(
987            app, bus, self.verbs, self.rules)
988
989    def find(self, **kw):
990        pass
991
992    def setup(self, **kw):
993        pass
994
995    def read_wsock(self, wsock, sock):
996        while True:
997            try:
998                incoming = wsock.receive()
999                if incoming:
1000                    # Read websocket, write to UNIX socket
1001                    sock.send(incoming)
1002            except Exception as e:
1003                sock.close()
1004                return
1005
1006    def read_sock(self, sock, wsock):
1007        max_sock_read_len = 4096
1008        while True:
1009            try:
1010                outgoing = sock.recv(max_sock_read_len)
1011                if outgoing:
1012                    # Read UNIX socket, write to websocket
1013                    wsock.send(outgoing)
1014            except Exception as e:
1015                wsock.close()
1016                return
1017
1018    def do_get(self):
1019        wsock = request.environ.get('wsgi.websocket')
1020        if not wsock:
1021            abort(400, 'Expected WebSocket based request.')
1022
1023        # A UNIX domain socket structure defines a 108-byte pathname. The
1024        # server in this case, obmc-console-server, expects a 108-byte path.
1025        socket_name = "\0obmc-console"
1026        trailing_bytes = "\0" * (108 - len(socket_name))
1027        socket_path = socket_name + trailing_bytes
1028        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1029
1030        try:
1031            sock.connect(socket_path)
1032        except Exception as e:
1033            abort(500, str(e))
1034
1035        wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1036        sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
1037        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
1038        gevent.joinall([wsock_reader, sock_reader, ping_sender])
1039
1040
1041class ImagePutHandler(RouteHandler):
1042    ''' Handles the /upload/image/<filename> route. '''
1043
1044    verbs = ['PUT']
1045    rules = ['/upload/image/<filename>']
1046    content_type = 'application/octet-stream'
1047
1048    def __init__(self, app, bus):
1049        super(ImagePutHandler, self).__init__(
1050            app, bus, self.verbs, self.rules, self.content_type)
1051
1052    def do_put(self, filename=''):
1053        return ImageUploadUtils.do_upload(filename)
1054
1055    def find(self, **kw):
1056        pass
1057
1058    def setup(self, **kw):
1059        pass
1060
1061
1062class DownloadDumpHandler(RouteHandler):
1063    ''' Handles the /download/dump route. '''
1064
1065    verbs = 'GET'
1066    rules = ['/download/dump/<dumpid>']
1067    content_type = 'application/octet-stream'
1068    dump_loc = '/var/lib/phosphor-debug-collector/dumps'
1069    suppress_json_resp = True
1070
1071    def __init__(self, app, bus):
1072        super(DownloadDumpHandler, self).__init__(
1073            app, bus, self.verbs, self.rules, self.content_type)
1074
1075    def do_get(self, dumpid):
1076        return self.do_download(dumpid)
1077
1078    def find(self, **kw):
1079        pass
1080
1081    def setup(self, **kw):
1082        pass
1083
1084    def do_download(self, dumpid):
1085        dump_loc = os.path.join(self.dump_loc, dumpid)
1086        if not os.path.exists(dump_loc):
1087            abort(404, "Path not found")
1088
1089        files = os.listdir(dump_loc)
1090        num_files = len(files)
1091        if num_files == 0:
1092            abort(404, "Dump not found")
1093
1094        return static_file(os.path.basename(files[0]), root=dump_loc,
1095                           download=True, mimetype=self.content_type)
1096
1097
1098class WebHandler(RouteHandler):
1099    ''' Handles the routes for the web UI files. '''
1100
1101    verbs = 'GET'
1102
1103    # Match only what we know are web files, so everything else
1104    # can get routed to the REST handlers.
1105    rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1106             '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1107             '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1108             '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1109             '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1110             '/<filename:re:.+\.ico>']
1111
1112    # The mimetypes module knows about most types, but not these
1113    content_types = {
1114        '.eot': 'application/vnd.ms-fontobject',
1115        '.woff': 'application/x-font-woff',
1116        '.woff2': 'application/x-font-woff2',
1117        '.ttf': 'application/x-font-ttf',
1118        '.map': 'application/json'
1119    }
1120
1121    _require_auth = None
1122    suppress_json_resp = True
1123
1124    def __init__(self, app, bus):
1125        super(WebHandler, self).__init__(
1126            app, bus, self.verbs, self.rules)
1127
1128    def get_type(self, filename):
1129        ''' Returns the content type and encoding for a file '''
1130
1131        content_type, encoding = mimetypes.guess_type(filename)
1132
1133        # Try our own list if mimetypes didn't recognize it
1134        if content_type is None:
1135            if filename[-3:] == '.gz':
1136                filename = filename[:-3]
1137            extension = filename[filename.rfind('.'):]
1138            content_type = self.content_types.get(extension, None)
1139
1140        return content_type, encoding
1141
1142    def do_get(self, filename='index.html'):
1143
1144        # If a gzipped version exists, use that instead.
1145        # Possible future enhancement: if the client doesn't
1146        # accept compressed files, unzip it ourselves before sending.
1147        if not os.path.exists(os.path.join(www_base_path, filename)):
1148            filename = filename + '.gz'
1149
1150        # Though bottle should protect us, ensure path is valid
1151        realpath = os.path.realpath(filename)
1152        if realpath[0] == '/':
1153            realpath = realpath[1:]
1154        if not os.path.exists(os.path.join(www_base_path, realpath)):
1155            abort(404, "Path not found")
1156
1157        mimetype, encoding = self.get_type(filename)
1158
1159        # Couldn't find the type - let static_file() deal with it,
1160        # though this should never happen.
1161        if mimetype is None:
1162            print("Can't figure out content-type for %s" % filename)
1163            mimetype = 'auto'
1164
1165        # This call will set several header fields for us,
1166        # including the charset if the type is text.
1167        response = static_file(filename, www_base_path, mimetype)
1168
1169        # static_file() will only set the encoding if the
1170        # mimetype was auto, so set it here.
1171        if encoding is not None:
1172            response.set_header('Content-Encoding', encoding)
1173
1174        return response
1175
1176    def find(self, **kw):
1177        pass
1178
1179    def setup(self, **kw):
1180        pass
1181
1182
1183class AuthorizationPlugin(object):
1184    ''' Invokes an optional list of authorization callbacks. '''
1185
1186    name = 'authorization'
1187    api = 2
1188
1189    class Compose:
1190        def __init__(self, validators, callback, session_mgr):
1191            self.validators = validators
1192            self.callback = callback
1193            self.session_mgr = session_mgr
1194
1195        def __call__(self, *a, **kw):
1196            sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1197            session = self.session_mgr.get_session(sid)
1198            if request.method != 'OPTIONS':
1199                for x in self.validators:
1200                    x(session, *a, **kw)
1201
1202            return self.callback(*a, **kw)
1203
1204    def apply(self, callback, route):
1205        undecorated = route.get_undecorated_callback()
1206        if not isinstance(undecorated, RouteHandler):
1207            return callback
1208
1209        auth_types = getattr(
1210            undecorated, '_require_auth', None)
1211        if not auth_types:
1212            return callback
1213
1214        return self.Compose(
1215            auth_types, callback, undecorated.app.session_handler)
1216
1217
1218class CorsPlugin(object):
1219    ''' Add CORS headers. '''
1220
1221    name = 'cors'
1222    api = 2
1223
1224    @staticmethod
1225    def process_origin():
1226        origin = request.headers.get('Origin')
1227        if origin:
1228            response.add_header('Access-Control-Allow-Origin', origin)
1229            response.add_header(
1230                'Access-Control-Allow-Credentials', 'true')
1231
1232    @staticmethod
1233    def process_method_and_headers(verbs):
1234        method = request.headers.get('Access-Control-Request-Method')
1235        headers = request.headers.get('Access-Control-Request-Headers')
1236        if headers:
1237            headers = [x.lower() for x in headers.split(',')]
1238
1239        if method in verbs \
1240                and headers == ['content-type']:
1241            response.add_header('Access-Control-Allow-Methods', method)
1242            response.add_header(
1243                'Access-Control-Allow-Headers', 'Content-Type')
1244            response.add_header('X-Frame-Options', 'deny')
1245            response.add_header('X-Content-Type-Options', 'nosniff')
1246            response.add_header('X-XSS-Protection', '1; mode=block')
1247            response.add_header(
1248                'Content-Security-Policy', "default-src 'self'")
1249            response.add_header(
1250                'Strict-Transport-Security',
1251                'max-age=31536000; includeSubDomains; preload')
1252
1253    def __init__(self, app):
1254        app.install_error_callback(self.error_callback)
1255
1256    def apply(self, callback, route):
1257        undecorated = route.get_undecorated_callback()
1258        if not isinstance(undecorated, RouteHandler):
1259            return callback
1260
1261        if not getattr(undecorated, '_enable_cors', None):
1262            return callback
1263
1264        def wrap(*a, **kw):
1265            self.process_origin()
1266            self.process_method_and_headers(undecorated._verbs)
1267            return callback(*a, **kw)
1268
1269        return wrap
1270
1271    def error_callback(self, **kw):
1272        self.process_origin()
1273
1274
1275class JsonApiRequestPlugin(object):
1276    ''' Ensures request content satisfies the OpenBMC json api format. '''
1277    name = 'json_api_request'
1278    api = 2
1279
1280    error_str = "Expecting request format { 'data': <value> }, got '%s'"
1281    type_error_str = "Unsupported Content-Type: '%s'"
1282    json_type = "application/json"
1283    request_methods = ['PUT', 'POST', 'PATCH']
1284
1285    @staticmethod
1286    def content_expected():
1287        return request.method in JsonApiRequestPlugin.request_methods
1288
1289    def validate_request(self):
1290        if request.content_length > 0 and \
1291                request.content_type != self.json_type:
1292            abort(415, self.type_error_str % request.content_type)
1293
1294        try:
1295            request.parameter_list = request.json.get('data')
1296        except ValueError as e:
1297            abort(400, str(e))
1298        except (AttributeError, KeyError, TypeError):
1299            abort(400, self.error_str % request.json)
1300
1301    def apply(self, callback, route):
1302        content_type = getattr(
1303            route.get_undecorated_callback(), '_content_type', None)
1304        if self.json_type != content_type:
1305            return callback
1306
1307        verbs = getattr(
1308            route.get_undecorated_callback(), '_verbs', None)
1309        if verbs is None:
1310            return callback
1311
1312        if not set(self.request_methods).intersection(verbs):
1313            return callback
1314
1315        def wrap(*a, **kw):
1316            if self.content_expected():
1317                self.validate_request()
1318            return callback(*a, **kw)
1319
1320        return wrap
1321
1322
1323class JsonApiRequestTypePlugin(object):
1324    ''' Ensures request content type satisfies the OpenBMC json api format. '''
1325    name = 'json_api_method_request'
1326    api = 2
1327
1328    error_str = "Expecting request format { 'data': %s }, got '%s'"
1329    json_type = "application/json"
1330
1331    def apply(self, callback, route):
1332        content_type = getattr(
1333            route.get_undecorated_callback(), '_content_type', None)
1334        if self.json_type != content_type:
1335            return callback
1336
1337        request_type = getattr(
1338            route.get_undecorated_callback(), 'request_type', None)
1339        if request_type is None:
1340            return callback
1341
1342        def validate_request():
1343            if not isinstance(request.parameter_list, request_type):
1344                abort(400, self.error_str % (str(request_type), request.json))
1345
1346        def wrap(*a, **kw):
1347            if JsonApiRequestPlugin.content_expected():
1348                validate_request()
1349            return callback(*a, **kw)
1350
1351        return wrap
1352
1353
1354class JsonErrorsPlugin(JSONPlugin):
1355    ''' Extend the Bottle JSONPlugin such that it also encodes error
1356        responses. '''
1357
1358    def __init__(self, app, **kw):
1359        super(JsonErrorsPlugin, self).__init__(**kw)
1360        self.json_opts = {
1361            x: y for x, y in kw.items()
1362            if x in ['indent', 'sort_keys']}
1363        app.install_error_callback(self.error_callback)
1364
1365    def error_callback(self, response_object, response_body, **kw):
1366        response_body['body'] = json.dumps(response_object, **self.json_opts)
1367        response.content_type = 'application/json'
1368
1369
1370class JsonApiResponsePlugin(object):
1371    ''' Emits responses in the OpenBMC json api format. '''
1372    name = 'json_api_response'
1373    api = 2
1374
1375    @staticmethod
1376    def has_body():
1377        return request.method not in ['OPTIONS']
1378
1379    def __init__(self, app):
1380        app.install_error_callback(self.error_callback)
1381
1382    def apply(self, callback, route):
1383        skip = getattr(
1384            route.get_undecorated_callback(), 'suppress_json_resp', None)
1385        if skip:
1386            return callback
1387
1388        def wrap(*a, **kw):
1389            data = callback(*a, **kw)
1390            if self.has_body():
1391                resp = {'data': data}
1392                resp['status'] = 'ok'
1393                resp['message'] = response.status_line
1394                return resp
1395        return wrap
1396
1397    def error_callback(self, error, response_object, **kw):
1398        response_object['message'] = error.status_line
1399        response_object['status'] = 'error'
1400        response_object.setdefault('data', {})['description'] = str(error.body)
1401        if error.status_code == 500:
1402            response_object['data']['exception'] = repr(error.exception)
1403            response_object['data']['traceback'] = error.traceback.splitlines()
1404
1405
1406class JsonpPlugin(object):
1407    ''' Json javascript wrapper. '''
1408    name = 'jsonp'
1409    api = 2
1410
1411    def __init__(self, app, **kw):
1412        app.install_error_callback(self.error_callback)
1413
1414    @staticmethod
1415    def to_jsonp(json):
1416        jwrapper = request.query.callback or None
1417        if(jwrapper):
1418            response.set_header('Content-Type', 'application/javascript')
1419            json = jwrapper + '(' + json + ');'
1420        return json
1421
1422    def apply(self, callback, route):
1423        def wrap(*a, **kw):
1424            return self.to_jsonp(callback(*a, **kw))
1425        return wrap
1426
1427    def error_callback(self, response_body, **kw):
1428        response_body['body'] = self.to_jsonp(response_body['body'])
1429
1430
1431class ContentCheckerPlugin(object):
1432    ''' Ensures that a route is associated with the expected content-type
1433        header. '''
1434    name = 'content_checker'
1435    api = 2
1436
1437    class Checker:
1438        def __init__(self, type, callback):
1439            self.expected_type = type
1440            self.callback = callback
1441            self.error_str = "Expecting content type '%s', got '%s'"
1442
1443        def __call__(self, *a, **kw):
1444            if request.method in ['PUT', 'POST', 'PATCH'] and \
1445                    self.expected_type and \
1446                    self.expected_type != request.content_type:
1447                abort(415, self.error_str % (self.expected_type,
1448                      request.content_type))
1449
1450            return self.callback(*a, **kw)
1451
1452    def apply(self, callback, route):
1453        content_type = getattr(
1454            route.get_undecorated_callback(), '_content_type', None)
1455
1456        return self.Checker(content_type, callback)
1457
1458class CheckURLPlugin(object):
1459    ''' Ensures that anything read and written using only urls listed in
1460        the url_config.json config file would allowed. '''
1461    name = 'url_checker'
1462    api = 2
1463
1464    def __init__(self):
1465        config_path = '/usr/share/rest-dbus/url_config.json'
1466        url_config = {}
1467        urls = {}
1468        self.pattern = {}
1469        if os.path.exists(config_path):
1470            try:
1471                with open(config_path) as data_file:
1472                    url_config = json.load(data_file)
1473                    urls = url_config.get("urls", ["*"])
1474                    self.pattern = '|'.join(fnmatch.translate(p) for p in urls)
1475                    self.pattern = re.compile(self.pattern)
1476            except ValueError as e:
1477                    abort(404, str(e))
1478        else:
1479            abort(404, "Config file path not found for Whitelisted URLs")
1480
1481    def apply(self, callback, route):
1482
1483        def wrap(*a, **kw):
1484                if self.pattern.match(request.path):
1485                    return callback(*a, **kw)
1486                abort(404,"Trying to access Blocked URL")
1487        return wrap
1488
1489class App(Bottle):
1490    def __init__(self, **kw):
1491        super(App, self).__init__(autojson=False)
1492
1493        self.have_wsock = kw.get('have_wsock', False)
1494        self.with_bmc_check = '--with-bmc-check' in sys.argv
1495
1496        self.bus = dbus.SystemBus()
1497        self.mapper = obmc.mapper.Mapper(self.bus)
1498        self.error_callbacks = []
1499
1500        self.install_hooks()
1501        self.install_plugins()
1502        self.create_handlers()
1503        self.install_handlers()
1504
1505    def install_plugins(self):
1506        # install json api plugins
1507        json_kw = {'indent': 2, 'sort_keys': True}
1508        self.install(AuthorizationPlugin())
1509        self.install(CorsPlugin(self))
1510        self.install(ContentCheckerPlugin())
1511        self.install(JsonpPlugin(self, **json_kw))
1512        self.install(JsonErrorsPlugin(self, **json_kw))
1513        self.install(JsonApiResponsePlugin(self))
1514        self.install(JsonApiRequestPlugin())
1515        self.install(JsonApiRequestTypePlugin())
1516        self.install(CheckURLPlugin())
1517
1518    def install_hooks(self):
1519        self.error_handler_type = type(self.default_error_handler)
1520        self.original_error_handler = self.default_error_handler
1521        self.default_error_handler = self.error_handler_type(
1522            self.custom_error_handler, self, Bottle)
1523
1524        self.real_router_match = self.router.match
1525        self.router.match = self.custom_router_match
1526        self.add_hook('before_request', self.strip_extra_slashes)
1527
1528    def create_handlers(self):
1529        # create route handlers
1530        self.session_handler = SessionHandler(self, self.bus)
1531        self.web_handler = WebHandler(self, self.bus)
1532        self.directory_handler = DirectoryHandler(self, self.bus)
1533        self.list_names_handler = ListNamesHandler(self, self.bus)
1534        self.list_handler = ListHandler(self, self.bus)
1535        self.method_handler = MethodHandler(self, self.bus)
1536        self.property_handler = PropertyHandler(self, self.bus)
1537        self.schema_handler = SchemaHandler(self, self.bus)
1538        self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1539        self.image_upload_put_handler = ImagePutHandler(self, self.bus)
1540        self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
1541        if self.have_wsock:
1542            self.event_handler = EventHandler(self, self.bus)
1543            self.host_console_handler = HostConsoleHandler(self, self.bus)
1544        self.instance_handler = InstanceHandler(self, self.bus)
1545
1546    def install_handlers(self):
1547        self.session_handler.install()
1548        self.web_handler.install()
1549        self.directory_handler.install()
1550        self.list_names_handler.install()
1551        self.list_handler.install()
1552        self.method_handler.install()
1553        self.property_handler.install()
1554        self.schema_handler.install()
1555        self.image_upload_post_handler.install()
1556        self.image_upload_put_handler.install()
1557        self.download_dump_get_handler.install()
1558        if self.have_wsock:
1559            self.event_handler.install()
1560            self.host_console_handler.install()
1561        # this has to come last, since it matches everything
1562        self.instance_handler.install()
1563
1564    def install_error_callback(self, callback):
1565        self.error_callbacks.insert(0, callback)
1566
1567    def custom_router_match(self, environ):
1568        ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1569            needed doesn't work for us since the instance rules match
1570            everything. This monkey-patch lets the route handler figure
1571            out which response is needed.  This could be accomplished
1572            with a hook but that would require calling the router match
1573            function twice.
1574        '''
1575        route, args = self.real_router_match(environ)
1576        if isinstance(route.callback, RouteHandler):
1577            route.callback._setup(**args)
1578
1579        return route, args
1580
1581    def custom_error_handler(self, res, error):
1582        ''' Allow plugins to modify error responses too via this custom
1583            error handler. '''
1584
1585        response_object = {}
1586        response_body = {}
1587        for x in self.error_callbacks:
1588            x(error=error,
1589                response_object=response_object,
1590                response_body=response_body)
1591
1592        return response_body.get('body', "")
1593
1594    @staticmethod
1595    def strip_extra_slashes():
1596        path = request.environ['PATH_INFO']
1597        trailing = ("", "/")[path[-1] == '/']
1598        parts = list(filter(bool, path.split('/')))
1599        request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
1600