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