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