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