xref: /openbmc/phosphor-rest-server/module/obmc/wsgi/apps/rest_dbus.py (revision 3c6d49a65b1928938b48866cb730d10577b496c4)
12c6fc760SBrad Bishop# Contributors Listed Below - COPYRIGHT 2016
22c6fc760SBrad Bishop# [+] International Business Machines Corp.
32c6fc760SBrad Bishop#
42c6fc760SBrad Bishop#
52c6fc760SBrad Bishop# Licensed under the Apache License, Version 2.0 (the "License");
62c6fc760SBrad Bishop# you may not use this file except in compliance with the License.
72c6fc760SBrad Bishop# You may obtain a copy of the License at
82c6fc760SBrad Bishop#
92c6fc760SBrad Bishop#     http://www.apache.org/licenses/LICENSE-2.0
102c6fc760SBrad Bishop#
112c6fc760SBrad Bishop# Unless required by applicable law or agreed to in writing, software
122c6fc760SBrad Bishop# distributed under the License is distributed on an "AS IS" BASIS,
132c6fc760SBrad Bishop# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
142c6fc760SBrad Bishop# implied. See the License for the specific language governing
152c6fc760SBrad Bishop# permissions and limitations under the License.
162c6fc760SBrad Bishop
172c6fc760SBrad Bishopimport os
18d08a4569SAlexander Filippovimport sys
192c6fc760SBrad Bishopimport dbus
202c6fc760SBrad Bishopimport dbus.exceptions
212c6fc760SBrad Bishopimport json
222c6fc760SBrad Bishopfrom xml.etree import ElementTree
232c6fc760SBrad Bishopfrom bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
249bc94997SJayanth Othayothfrom bottle import static_file
252c6fc760SBrad Bishopimport obmc.utils.misc
262c6fc760SBrad Bishopfrom obmc.dbuslib.introspection import IntrospectionNodeParser
272c6fc760SBrad Bishopimport obmc.mapper
282c6fc760SBrad Bishopimport spwd
292c6fc760SBrad Bishopimport grp
302c6fc760SBrad Bishopimport crypt
311af301a0SDeepak Kodihalliimport tempfile
320bdef95dSLeonel Gonzalezimport re
33d41643ecSMatt Spinlerimport mimetypes
34639b502eSDeepak Kodihallihave_wsock = True
35639b502eSDeepak Kodihallitry:
36639b502eSDeepak Kodihalli    from geventwebsocket import WebSocketError
37639b502eSDeepak Kodihalliexcept ImportError:
38639b502eSDeepak Kodihalli    have_wsock = False
39639b502eSDeepak Kodihalliif have_wsock:
40639b502eSDeepak Kodihalli    from dbus.mainloop.glib import DBusGMainLoop
41639b502eSDeepak Kodihalli    DBusGMainLoop(set_as_default=True)
42249d1327SCamVan Nguyen    # TODO: openbmc/openbmc#2994 remove python 2 support
43249d1327SCamVan Nguyen    try:  # python 2
44639b502eSDeepak Kodihalli        import gobject
45249d1327SCamVan Nguyen    except ImportError:  # python 3
46249d1327SCamVan Nguyen        from gi.repository import GObject as gobject
47639b502eSDeepak Kodihalli    import gevent
485c518f63SDeepak Kodihalli    from gevent import socket
495c518f63SDeepak Kodihalli    from gevent import Greenlet
502c6fc760SBrad Bishop
51f92cf4dbSAdriana KobylakDBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
522c6fc760SBrad BishopDBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
53a8b05d16SAdriana KobylakDBUS_PROPERTY_READONLY = 'org.freedesktop.DBus.Error.PropertyReadOnly'
542c6fc760SBrad BishopDBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
552c6fc760SBrad BishopDBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
566075bb48SDeepak KodihalliDELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
5753693891SAdriana KobylakSOFTWARE_PATH = '/xyz/openbmc_project/software'
58bec10c20SJayashankar PadathWEBSOCKET_TIMEOUT = 45
592c6fc760SBrad Bishop
602c6fc760SBrad Bishop_4034_msg = "The specified %s cannot be %s: '%s'"
612c6fc760SBrad Bishop
62d41643ecSMatt Spinlerwww_base_path = '/usr/share/www/'
63d41643ecSMatt Spinler
642c6fc760SBrad Bishop
652c6fc760SBrad Bishopdef valid_user(session, *a, **kw):
662c6fc760SBrad Bishop    ''' Authorization plugin callback that checks
672c6fc760SBrad Bishop    that the user is logged in. '''
682c6fc760SBrad Bishop    if session is None:
69dc3fbfa7SBrad Bishop        abort(401, 'Login required')
702c6fc760SBrad Bishop
712c6fc760SBrad Bishop
720bdef95dSLeonel Gonzalezdef get_type_signature_by_introspection(bus, service, object_path,
730bdef95dSLeonel Gonzalez                                        property_name):
740bdef95dSLeonel Gonzalez    obj = bus.get_object(service, object_path)
750bdef95dSLeonel Gonzalez    iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
760bdef95dSLeonel Gonzalez    xml_string = iface.Introspect()
770bdef95dSLeonel Gonzalez    for child in ElementTree.fromstring(xml_string):
780bdef95dSLeonel Gonzalez        # Iterate over each interfaces's properties to find
790bdef95dSLeonel Gonzalez        # matching property_name, and return its signature string
800bdef95dSLeonel Gonzalez        if child.tag == 'interface':
810bdef95dSLeonel Gonzalez            for i in child.iter():
820bdef95dSLeonel Gonzalez                if ('name' in i.attrib) and \
830bdef95dSLeonel Gonzalez                   (i.attrib['name'] == property_name):
840bdef95dSLeonel Gonzalez                    type_signature = i.attrib['type']
850bdef95dSLeonel Gonzalez                    return type_signature
860bdef95dSLeonel Gonzalez
870bdef95dSLeonel Gonzalez
88a6a8a4c1SRatan Guptadef get_method_signature(bus, service, object_path, interface, method):
89a6a8a4c1SRatan Gupta    obj = bus.get_object(service, object_path)
90a6a8a4c1SRatan Gupta    iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
91a6a8a4c1SRatan Gupta    xml_string = iface.Introspect()
92a6a8a4c1SRatan Gupta    arglist = []
93a6a8a4c1SRatan Gupta
94a6a8a4c1SRatan Gupta    root = ElementTree.fromstring(xml_string)
95a6a8a4c1SRatan Gupta    for dbus_intf in root.findall('interface'):
96a6a8a4c1SRatan Gupta        if (dbus_intf.get('name') == interface):
97a6a8a4c1SRatan Gupta            for dbus_method in dbus_intf.findall('method'):
98a6a8a4c1SRatan Gupta                if(dbus_method.get('name') == method):
99a6a8a4c1SRatan Gupta                    for arg in dbus_method.findall('arg'):
100*3c6d49a6SRatan Gupta                        if (arg.get('direction') == 'in'):
101a6a8a4c1SRatan Gupta                            arglist.append(arg.get('type'))
102a6a8a4c1SRatan Gupta                    return arglist
103a6a8a4c1SRatan Gupta
104a6a8a4c1SRatan Gupta
1050bdef95dSLeonel Gonzalezdef split_struct_signature(signature):
1060bdef95dSLeonel Gonzalez    struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
1070bdef95dSLeonel Gonzalez    struct_matches = re.findall(struct_regex, signature)
1080bdef95dSLeonel Gonzalez    return struct_matches
1090bdef95dSLeonel Gonzalez
1100bdef95dSLeonel Gonzalez
1110bdef95dSLeonel Gonzalezdef convert_type(signature, value):
1120bdef95dSLeonel Gonzalez    # Basic Types
1130bdef95dSLeonel Gonzalez    converted_value = None
1140bdef95dSLeonel Gonzalez    converted_container = None
115249d1327SCamVan Nguyen    # TODO: openbmc/openbmc#2994 remove python 2 support
116249d1327SCamVan Nguyen    try:  # python 2
1170bdef95dSLeonel Gonzalez        basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
1180bdef95dSLeonel Gonzalez                       'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
1190bdef95dSLeonel Gonzalez                       't': dbus.UInt64, 'd': float, 's': str}
120249d1327SCamVan Nguyen    except NameError:  # python 3
121249d1327SCamVan Nguyen        basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
122249d1327SCamVan Nguyen                       'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32,
123249d1327SCamVan Nguyen                       't': dbus.UInt64, 'd': float, 's': str}
1240bdef95dSLeonel Gonzalez    array_matches = re.match(r'a\((\S+)\)', signature)
1250bdef95dSLeonel Gonzalez    struct_matches = re.match(r'\((\S+)\)', signature)
1260bdef95dSLeonel Gonzalez    dictionary_matches = re.match(r'a{(\S+)}', signature)
1270bdef95dSLeonel Gonzalez    if signature in basic_types:
1280bdef95dSLeonel Gonzalez        converted_value = basic_types[signature](value)
1290bdef95dSLeonel Gonzalez        return converted_value
1300bdef95dSLeonel Gonzalez    # Array
1310bdef95dSLeonel Gonzalez    if array_matches:
1320bdef95dSLeonel Gonzalez        element_type = array_matches.group(1)
1330bdef95dSLeonel Gonzalez        converted_container = list()
1340bdef95dSLeonel Gonzalez        # Test if value is a list
1350bdef95dSLeonel Gonzalez        # to avoid iterating over each character in a string.
1360bdef95dSLeonel Gonzalez        # Iterate over each item and convert type
1370bdef95dSLeonel Gonzalez        if isinstance(value, list):
1380bdef95dSLeonel Gonzalez            for i in value:
1390bdef95dSLeonel Gonzalez                converted_element = convert_type(element_type, i)
1400bdef95dSLeonel Gonzalez                converted_container.append(converted_element)
1410bdef95dSLeonel Gonzalez        # Convert non-sequence to expected type, and append to list
1420bdef95dSLeonel Gonzalez        else:
1430bdef95dSLeonel Gonzalez            converted_element = convert_type(element_type, value)
1440bdef95dSLeonel Gonzalez            converted_container.append(converted_element)
1450bdef95dSLeonel Gonzalez        return converted_container
1460bdef95dSLeonel Gonzalez    # Struct
1470bdef95dSLeonel Gonzalez    if struct_matches:
1480bdef95dSLeonel Gonzalez        element_types = struct_matches.group(1)
1490bdef95dSLeonel Gonzalez        split_element_types = split_struct_signature(element_types)
1500bdef95dSLeonel Gonzalez        converted_container = list()
1510bdef95dSLeonel Gonzalez        # Test if value is a list
1520bdef95dSLeonel Gonzalez        if isinstance(value, list):
1530bdef95dSLeonel Gonzalez            for index, val in enumerate(value):
1540bdef95dSLeonel Gonzalez                converted_element = convert_type(split_element_types[index],
1550bdef95dSLeonel Gonzalez                                                 value[index])
1560bdef95dSLeonel Gonzalez                converted_container.append(converted_element)
1570bdef95dSLeonel Gonzalez        else:
1580bdef95dSLeonel Gonzalez            converted_element = convert_type(element_types, value)
1590bdef95dSLeonel Gonzalez            converted_container.append(converted_element)
1600bdef95dSLeonel Gonzalez        return tuple(converted_container)
1610bdef95dSLeonel Gonzalez    # Dictionary
1620bdef95dSLeonel Gonzalez    if dictionary_matches:
1630bdef95dSLeonel Gonzalez        element_types = dictionary_matches.group(1)
1640bdef95dSLeonel Gonzalez        split_element_types = split_struct_signature(element_types)
1650bdef95dSLeonel Gonzalez        converted_container = dict()
1660bdef95dSLeonel Gonzalez        # Convert each element of dict
167249d1327SCamVan Nguyen        for key, val in value.items():
1680bdef95dSLeonel Gonzalez            converted_key = convert_type(split_element_types[0], key)
1690bdef95dSLeonel Gonzalez            converted_val = convert_type(split_element_types[1], val)
1700bdef95dSLeonel Gonzalez            converted_container[converted_key] = converted_val
1710bdef95dSLeonel Gonzalez        return converted_container
1720bdef95dSLeonel Gonzalez
1730bdef95dSLeonel Gonzalez
174bec10c20SJayashankar Padathdef send_ws_ping(wsock, timeout) :
175bec10c20SJayashankar Padath    # Most webservers close websockets after 60 seconds of
176bec10c20SJayashankar Padath    # inactivity. Make sure to send a ping before that.
177bec10c20SJayashankar Padath    payload = "ping"
178bec10c20SJayashankar Padath    # the ping payload can be anything, the receiver has to just
179bec10c20SJayashankar Padath    # return the same back.
180bec10c20SJayashankar Padath    while True:
181bec10c20SJayashankar Padath        gevent.sleep(timeout)
182bec10c20SJayashankar Padath        try:
183bec10c20SJayashankar Padath            if wsock:
184bec10c20SJayashankar Padath                wsock.send_frame(payload, wsock.OPCODE_PING)
185bec10c20SJayashankar Padath        except Exception as e:
186bec10c20SJayashankar Padath            wsock.close()
187bec10c20SJayashankar Padath            return
188bec10c20SJayashankar Padath
189bec10c20SJayashankar Padath
1902c6fc760SBrad Bishopclass UserInGroup:
1912c6fc760SBrad Bishop    ''' Authorization plugin callback that checks that the user is logged in
1922c6fc760SBrad Bishop    and a member of a group. '''
1932c6fc760SBrad Bishop    def __init__(self, group):
1942c6fc760SBrad Bishop        self.group = group
1952c6fc760SBrad Bishop
1962c6fc760SBrad Bishop    def __call__(self, session, *a, **kw):
1972c6fc760SBrad Bishop        valid_user(session, *a, **kw)
1982c6fc760SBrad Bishop        res = False
1992c6fc760SBrad Bishop
2002c6fc760SBrad Bishop        try:
2012c6fc760SBrad Bishop            res = session['user'] in grp.getgrnam(self.group)[3]
2022c6fc760SBrad Bishop        except KeyError:
2032c6fc760SBrad Bishop            pass
2042c6fc760SBrad Bishop
2052c6fc760SBrad Bishop        if not res:
2062c6fc760SBrad Bishop            abort(403, 'Insufficient access')
2072c6fc760SBrad Bishop
2082c6fc760SBrad Bishop
2092c6fc760SBrad Bishopclass RouteHandler(object):
2102c6fc760SBrad Bishop    _require_auth = obmc.utils.misc.makelist(valid_user)
211d0c404a6SBrad Bishop    _enable_cors = True
2122c6fc760SBrad Bishop
21383afbaffSDeepak Kodihalli    def __init__(self, app, bus, verbs, rules, content_type=''):
2142c6fc760SBrad Bishop        self.app = app
2152c6fc760SBrad Bishop        self.bus = bus
2162c6fc760SBrad Bishop        self.mapper = obmc.mapper.Mapper(bus)
2172c6fc760SBrad Bishop        self._verbs = obmc.utils.misc.makelist(verbs)
2182c6fc760SBrad Bishop        self._rules = rules
21983afbaffSDeepak Kodihalli        self._content_type = content_type
2202c6fc760SBrad Bishop
22188c76a4dSBrad Bishop        if 'GET' in self._verbs:
22288c76a4dSBrad Bishop            self._verbs = list(set(self._verbs + ['HEAD']))
223d4c1c55cSBrad Bishop        if 'OPTIONS' not in self._verbs:
224d4c1c55cSBrad Bishop            self._verbs.append('OPTIONS')
22588c76a4dSBrad Bishop
2262c6fc760SBrad Bishop    def _setup(self, **kw):
2272c6fc760SBrad Bishop        request.route_data = {}
228d4c1c55cSBrad Bishop
2292c6fc760SBrad Bishop        if request.method in self._verbs:
230d4c1c55cSBrad Bishop            if request.method != 'OPTIONS':
2312c6fc760SBrad Bishop                return self.setup(**kw)
23288c76a4dSBrad Bishop
233d4c1c55cSBrad Bishop            # Javascript implementations will not send credentials
234d4c1c55cSBrad Bishop            # with an OPTIONS request.  Don't help malicious clients
235d4c1c55cSBrad Bishop            # by checking the path here and returning a 404 if the
236d4c1c55cSBrad Bishop            # path doesn't exist.
237d4c1c55cSBrad Bishop            return None
23888c76a4dSBrad Bishop
239d4c1c55cSBrad Bishop        # Return 405
2402c6fc760SBrad Bishop        raise HTTPError(
2412c6fc760SBrad Bishop            405, "Method not allowed.", Allow=','.join(self._verbs))
2422c6fc760SBrad Bishop
2432c6fc760SBrad Bishop    def __call__(self, **kw):
2442c6fc760SBrad Bishop        return getattr(self, 'do_' + request.method.lower())(**kw)
2452c6fc760SBrad Bishop
24688c76a4dSBrad Bishop    def do_head(self, **kw):
24788c76a4dSBrad Bishop        return self.do_get(**kw)
24888c76a4dSBrad Bishop
249d4c1c55cSBrad Bishop    def do_options(self, **kw):
250d4c1c55cSBrad Bishop        for v in self._verbs:
251d4c1c55cSBrad Bishop            response.set_header(
252d4c1c55cSBrad Bishop                'Allow',
253d4c1c55cSBrad Bishop                ','.join(self._verbs))
254d4c1c55cSBrad Bishop        return None
255d4c1c55cSBrad Bishop
2562c6fc760SBrad Bishop    def install(self):
2572c6fc760SBrad Bishop        self.app.route(
2582c6fc760SBrad Bishop            self._rules, callback=self,
259d4c1c55cSBrad Bishop            method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
2602c6fc760SBrad Bishop
2612c6fc760SBrad Bishop    @staticmethod
2622c6fc760SBrad Bishop    def try_mapper_call(f, callback=None, **kw):
2632c6fc760SBrad Bishop        try:
2642c6fc760SBrad Bishop            return f(**kw)
265249d1327SCamVan Nguyen        except dbus.exceptions.DBusException as e:
266fce7756cSBrad Bishop            if e.get_dbus_name() == \
267fce7756cSBrad Bishop                    'org.freedesktop.DBus.Error.ObjectPathInUse':
268fce7756cSBrad Bishop                abort(503, str(e))
2692c6fc760SBrad Bishop            if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
2702c6fc760SBrad Bishop                raise
2712c6fc760SBrad Bishop            if callback is None:
2722c6fc760SBrad Bishop                def callback(e, **kw):
2732c6fc760SBrad Bishop                    abort(404, str(e))
2742c6fc760SBrad Bishop
2752c6fc760SBrad Bishop            callback(e, **kw)
2762c6fc760SBrad Bishop
2772c6fc760SBrad Bishop    @staticmethod
2782c6fc760SBrad Bishop    def try_properties_interface(f, *a):
2792c6fc760SBrad Bishop        try:
2802c6fc760SBrad Bishop            return f(*a)
281249d1327SCamVan Nguyen        except dbus.exceptions.DBusException as e:
282f92cf4dbSAdriana Kobylak            if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name():
2832c6fc760SBrad Bishop                # interface doesn't have any properties
2842c6fc760SBrad Bishop                return None
2852c6fc760SBrad Bishop            if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
2862c6fc760SBrad Bishop                # properties interface not implemented at all
2872c6fc760SBrad Bishop                return None
2882c6fc760SBrad Bishop            raise
2892c6fc760SBrad Bishop
2902c6fc760SBrad Bishop
2912c6fc760SBrad Bishopclass DirectoryHandler(RouteHandler):
2922c6fc760SBrad Bishop    verbs = 'GET'
2932c6fc760SBrad Bishop    rules = '<path:path>/'
2946e1ca530SDeepak Kodihalli    suppress_logging = True
2952c6fc760SBrad Bishop
2962c6fc760SBrad Bishop    def __init__(self, app, bus):
2972c6fc760SBrad Bishop        super(DirectoryHandler, self).__init__(
298c431e1afSBrad Bishop            app, bus, self.verbs, self.rules)
2992c6fc760SBrad Bishop
3002c6fc760SBrad Bishop    def find(self, path='/'):
3012c6fc760SBrad Bishop        return self.try_mapper_call(
3022c6fc760SBrad Bishop            self.mapper.get_subtree_paths, path=path, depth=1)
3032c6fc760SBrad Bishop
3042c6fc760SBrad Bishop    def setup(self, path='/'):
3052c6fc760SBrad Bishop        request.route_data['map'] = self.find(path)
3062c6fc760SBrad Bishop
3072c6fc760SBrad Bishop    def do_get(self, path='/'):
3082c6fc760SBrad Bishop        return request.route_data['map']
3092c6fc760SBrad Bishop
3102c6fc760SBrad Bishop
3112c6fc760SBrad Bishopclass ListNamesHandler(RouteHandler):
3122c6fc760SBrad Bishop    verbs = 'GET'
3132c6fc760SBrad Bishop    rules = ['/list', '<path:path>/list']
3146e1ca530SDeepak Kodihalli    suppress_logging = True
3152c6fc760SBrad Bishop
3162c6fc760SBrad Bishop    def __init__(self, app, bus):
3172c6fc760SBrad Bishop        super(ListNamesHandler, self).__init__(
318c431e1afSBrad Bishop            app, bus, self.verbs, self.rules)
3192c6fc760SBrad Bishop
3202c6fc760SBrad Bishop    def find(self, path='/'):
321249d1327SCamVan Nguyen        return list(self.try_mapper_call(
322249d1327SCamVan Nguyen            self.mapper.get_subtree, path=path).keys())
3232c6fc760SBrad Bishop
3242c6fc760SBrad Bishop    def setup(self, path='/'):
3252c6fc760SBrad Bishop        request.route_data['map'] = self.find(path)
3262c6fc760SBrad Bishop
3272c6fc760SBrad Bishop    def do_get(self, path='/'):
3282c6fc760SBrad Bishop        return request.route_data['map']
3292c6fc760SBrad Bishop
3302c6fc760SBrad Bishop
3312c6fc760SBrad Bishopclass ListHandler(RouteHandler):
3322c6fc760SBrad Bishop    verbs = 'GET'
3332c6fc760SBrad Bishop    rules = ['/enumerate', '<path:path>/enumerate']
3346e1ca530SDeepak Kodihalli    suppress_logging = True
3352c6fc760SBrad Bishop
3362c6fc760SBrad Bishop    def __init__(self, app, bus):
3372c6fc760SBrad Bishop        super(ListHandler, self).__init__(
338c431e1afSBrad Bishop            app, bus, self.verbs, self.rules)
3392c6fc760SBrad Bishop
3402c6fc760SBrad Bishop    def find(self, path='/'):
3412c6fc760SBrad Bishop        return self.try_mapper_call(
3422c6fc760SBrad Bishop            self.mapper.get_subtree, path=path)
3432c6fc760SBrad Bishop
3442c6fc760SBrad Bishop    def setup(self, path='/'):
3452c6fc760SBrad Bishop        request.route_data['map'] = self.find(path)
3462c6fc760SBrad Bishop
3472c6fc760SBrad Bishop    def do_get(self, path='/'):
3482c6fc760SBrad Bishop        return {x: y for x, y in self.mapper.enumerate_subtree(
3492c6fc760SBrad Bishop                path,
3502c6fc760SBrad Bishop                mapper_data=request.route_data['map']).dataitems()}
3512c6fc760SBrad Bishop
3522c6fc760SBrad Bishop
3532c6fc760SBrad Bishopclass MethodHandler(RouteHandler):
3542c6fc760SBrad Bishop    verbs = 'POST'
3552c6fc760SBrad Bishop    rules = '<path:path>/action/<method>'
3562c6fc760SBrad Bishop    request_type = list
35783afbaffSDeepak Kodihalli    content_type = 'application/json'
3582c6fc760SBrad Bishop
3592c6fc760SBrad Bishop    def __init__(self, app, bus):
3602c6fc760SBrad Bishop        super(MethodHandler, self).__init__(
36183afbaffSDeepak Kodihalli            app, bus, self.verbs, self.rules, self.content_type)
362a6a8a4c1SRatan Gupta        self.service = ''
363a6a8a4c1SRatan Gupta        self.interface = ''
3642c6fc760SBrad Bishop
3652c6fc760SBrad Bishop    def find(self, path, method):
3663a00b1fdSSaqib Khan        method_list = []
367313aadb3SGunnar Mills        buses = self.try_mapper_call(
3682c6fc760SBrad Bishop            self.mapper.get_object, path=path)
369313aadb3SGunnar Mills        for items in buses.items():
3702c6fc760SBrad Bishop            m = self.find_method_on_bus(path, method, *items)
3712c6fc760SBrad Bishop            if m:
3723a00b1fdSSaqib Khan                method_list.append(m)
373765c2c8bSNagaraju Goruganti        if method_list:
3743a00b1fdSSaqib Khan            return method_list
3752c6fc760SBrad Bishop
3762c6fc760SBrad Bishop        abort(404, _4034_msg % ('method', 'found', method))
3772c6fc760SBrad Bishop
3782c6fc760SBrad Bishop    def setup(self, path, method):
3793a00b1fdSSaqib Khan        request.route_data['map'] = self.find(path, method)
3802c6fc760SBrad Bishop
381bc0c6738SMarri Devender Rao    def do_post(self, path, method, retry=True):
3822c6fc760SBrad Bishop        try:
383765c2c8bSNagaraju Goruganti            args = []
3842c6fc760SBrad Bishop            if request.parameter_list:
385765c2c8bSNagaraju Goruganti                args = request.parameter_list
386765c2c8bSNagaraju Goruganti            # To see if the return type is capable of being merged
387765c2c8bSNagaraju Goruganti            if len(request.route_data['map']) > 1:
388765c2c8bSNagaraju Goruganti                results = None
389765c2c8bSNagaraju Goruganti                for item in request.route_data['map']:
390765c2c8bSNagaraju Goruganti                    tmp = item(*args)
391765c2c8bSNagaraju Goruganti                    if not results:
392765c2c8bSNagaraju Goruganti                        if tmp is not None:
393765c2c8bSNagaraju Goruganti                            results = type(tmp)()
394765c2c8bSNagaraju Goruganti                    if isinstance(results, dict):
395765c2c8bSNagaraju Goruganti                        results = results.update(tmp)
396765c2c8bSNagaraju Goruganti                    elif isinstance(results, list):
397765c2c8bSNagaraju Goruganti                        results = results + tmp
398765c2c8bSNagaraju Goruganti                    elif isinstance(results, type(None)):
399765c2c8bSNagaraju Goruganti                        results = None
4002c6fc760SBrad Bishop                    else:
401765c2c8bSNagaraju Goruganti                        abort(501, 'Don\'t know how to merge method call '
402765c2c8bSNagaraju Goruganti                                   'results of {}'.format(type(tmp)))
403765c2c8bSNagaraju Goruganti                return results
404765c2c8bSNagaraju Goruganti            # There is only one method
405765c2c8bSNagaraju Goruganti            return request.route_data['map'][0](*args)
4062c6fc760SBrad Bishop
407249d1327SCamVan Nguyen        except dbus.exceptions.DBusException as e:
408a6a8a4c1SRatan Gupta            paramlist = []
409b7fca9bcSBrad Bishop            if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
410a6a8a4c1SRatan Gupta
411a6a8a4c1SRatan Gupta                signature_list = get_method_signature(self.bus, self.service,
412a6a8a4c1SRatan Gupta                                                      path, self.interface,
413a6a8a4c1SRatan Gupta                                                      method)
414a6a8a4c1SRatan Gupta                if not signature_list:
415a6a8a4c1SRatan Gupta                    abort(400, "Failed to get method signature: %s" % str(e))
416a6a8a4c1SRatan Gupta                if len(signature_list) != len(request.parameter_list):
417a6a8a4c1SRatan Gupta                    abort(400, "Invalid number of args")
418a6a8a4c1SRatan Gupta                converted_value = None
419a6a8a4c1SRatan Gupta                try:
420a6a8a4c1SRatan Gupta                    for index, expected_type in enumerate(signature_list):
421a6a8a4c1SRatan Gupta                        value = request.parameter_list[index]
422a6a8a4c1SRatan Gupta                        converted_value = convert_type(expected_type, value)
423a6a8a4c1SRatan Gupta                        paramlist.append(converted_value)
424a6a8a4c1SRatan Gupta                    request.parameter_list = paramlist
425bc0c6738SMarri Devender Rao                    self.do_post(path, method, False)
426a6a8a4c1SRatan Gupta                    return
427a6a8a4c1SRatan Gupta                except Exception as ex:
428ab404fa2SNagaraju Goruganti                    abort(400, "Bad Request/Invalid Args given")
4292c6fc760SBrad Bishop                abort(400, str(e))
430a6a8a4c1SRatan Gupta
4312c6fc760SBrad Bishop            if e.get_dbus_name() == DBUS_TYPE_ERROR:
4322c6fc760SBrad Bishop                abort(400, str(e))
4332c6fc760SBrad Bishop            raise
4342c6fc760SBrad Bishop
4352c6fc760SBrad Bishop    @staticmethod
4362c6fc760SBrad Bishop    def find_method_in_interface(method, obj, interface, methods):
4372c6fc760SBrad Bishop        if methods is None:
4382c6fc760SBrad Bishop            return None
4392c6fc760SBrad Bishop
440249d1327SCamVan Nguyen        method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
4412c6fc760SBrad Bishop        if method is not None:
4422c6fc760SBrad Bishop            iface = dbus.Interface(obj, interface)
4432c6fc760SBrad Bishop            return iface.get_dbus_method(method)
4442c6fc760SBrad Bishop
4452c6fc760SBrad Bishop    def find_method_on_bus(self, path, method, bus, interfaces):
4462c6fc760SBrad Bishop        obj = self.bus.get_object(bus, path, introspect=False)
4472c6fc760SBrad Bishop        iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
4482c6fc760SBrad Bishop        data = iface.Introspect()
4492c6fc760SBrad Bishop        parser = IntrospectionNodeParser(
4502c6fc760SBrad Bishop            ElementTree.fromstring(data),
451aeb995daSBrad Bishop            intf_match=lambda x: x in interfaces)
452249d1327SCamVan Nguyen        for x, y in parser.get_interfaces().items():
4532c6fc760SBrad Bishop            m = self.find_method_in_interface(
4542c6fc760SBrad Bishop                method, obj, x, y.get('method'))
4552c6fc760SBrad Bishop            if m:
456a6a8a4c1SRatan Gupta                self.service = bus
457a6a8a4c1SRatan Gupta                self.interface = x
4582c6fc760SBrad Bishop                return m
4592c6fc760SBrad Bishop
4602c6fc760SBrad Bishop
4612c6fc760SBrad Bishopclass PropertyHandler(RouteHandler):
4622c6fc760SBrad Bishop    verbs = ['PUT', 'GET']
4632c6fc760SBrad Bishop    rules = '<path:path>/attr/<prop>'
46483afbaffSDeepak Kodihalli    content_type = 'application/json'
4652c6fc760SBrad Bishop
4662c6fc760SBrad Bishop    def __init__(self, app, bus):
4672c6fc760SBrad Bishop        super(PropertyHandler, self).__init__(
46883afbaffSDeepak Kodihalli            app, bus, self.verbs, self.rules, self.content_type)
4692c6fc760SBrad Bishop
4702c6fc760SBrad Bishop    def find(self, path, prop):
4712c6fc760SBrad Bishop        self.app.instance_handler.setup(path)
4722c6fc760SBrad Bishop        obj = self.app.instance_handler.do_get(path)
47356ad87f3SBrad Bishop        real_name = obmc.utils.misc.find_case_insensitive(
474249d1327SCamVan Nguyen            prop, list(obj.keys()))
4752c6fc760SBrad Bishop
47656ad87f3SBrad Bishop        if not real_name:
47756ad87f3SBrad Bishop            if request.method == 'PUT':
47856ad87f3SBrad Bishop                abort(403, _4034_msg % ('property', 'created', prop))
47956ad87f3SBrad Bishop            else:
48056ad87f3SBrad Bishop                abort(404, _4034_msg % ('property', 'found', prop))
48156ad87f3SBrad Bishop        return real_name, {path: obj}
4822c6fc760SBrad Bishop
4832c6fc760SBrad Bishop    def setup(self, path, prop):
48456ad87f3SBrad Bishop        name, obj = self.find(path, prop)
48556ad87f3SBrad Bishop        request.route_data['obj'] = obj
48656ad87f3SBrad Bishop        request.route_data['name'] = name
4872c6fc760SBrad Bishop
4882c6fc760SBrad Bishop    def do_get(self, path, prop):
48956ad87f3SBrad Bishop        name = request.route_data['name']
49056ad87f3SBrad Bishop        return request.route_data['obj'][path][name]
4912c6fc760SBrad Bishop
492bc0c6738SMarri Devender Rao    def do_put(self, path, prop, value=None, retry=True):
4932c6fc760SBrad Bishop        if value is None:
4942c6fc760SBrad Bishop            value = request.parameter_list
4952c6fc760SBrad Bishop
4962c6fc760SBrad Bishop        prop, iface, properties_iface = self.get_host_interface(
4972c6fc760SBrad Bishop            path, prop, request.route_data['map'][path])
4982c6fc760SBrad Bishop        try:
4992c6fc760SBrad Bishop            properties_iface.Set(iface, prop, value)
500249d1327SCamVan Nguyen        except ValueError as e:
5012c6fc760SBrad Bishop            abort(400, str(e))
502249d1327SCamVan Nguyen        except dbus.exceptions.DBusException as e:
503a8b05d16SAdriana Kobylak            if e.get_dbus_name() == DBUS_PROPERTY_READONLY:
504a8b05d16SAdriana Kobylak                abort(403, str(e))
505b7fca9bcSBrad Bishop            if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
5060bdef95dSLeonel Gonzalez                bus_name = properties_iface.bus_name
5070bdef95dSLeonel Gonzalez                expected_type = get_type_signature_by_introspection(self.bus,
5080bdef95dSLeonel Gonzalez                                                                    bus_name,
5090bdef95dSLeonel Gonzalez                                                                    path,
5100bdef95dSLeonel Gonzalez                                                                    prop)
5110bdef95dSLeonel Gonzalez                if not expected_type:
5120bdef95dSLeonel Gonzalez                    abort(403, "Failed to get expected type: %s" % str(e))
5130bdef95dSLeonel Gonzalez                converted_value = None
5140bdef95dSLeonel Gonzalez                try:
5150bdef95dSLeonel Gonzalez                    converted_value = convert_type(expected_type, value)
5160bdef95dSLeonel Gonzalez                except Exception as ex:
5170bdef95dSLeonel Gonzalez                    abort(403, "Failed to convert %s to type %s" %
5180bdef95dSLeonel Gonzalez                          (value, expected_type))
5191eea5c3bSLei YU                try:
5201eea5c3bSLei YU                    self.do_put(path, prop, converted_value, False)
5211eea5c3bSLei YU                    return
5221eea5c3bSLei YU                except Exception as ex:
5231eea5c3bSLei YU                    abort(403, str(ex))
5241eea5c3bSLei YU
5252c6fc760SBrad Bishop                abort(403, str(e))
5262c6fc760SBrad Bishop            raise
5272c6fc760SBrad Bishop
5282c6fc760SBrad Bishop    def get_host_interface(self, path, prop, bus_info):
529249d1327SCamVan Nguyen        for bus, interfaces in bus_info.items():
5302c6fc760SBrad Bishop            obj = self.bus.get_object(bus, path, introspect=True)
5312c6fc760SBrad Bishop            properties_iface = dbus.Interface(
5322c6fc760SBrad Bishop                obj, dbus_interface=dbus.PROPERTIES_IFACE)
5332c6fc760SBrad Bishop
53444573ab2SAdriana Kobylak            try:
5352c6fc760SBrad Bishop                info = self.get_host_interface_on_bus(
5362c6fc760SBrad Bishop                    path, prop, properties_iface, bus, interfaces)
53744573ab2SAdriana Kobylak            except Exception:
53844573ab2SAdriana Kobylak                continue
5392c6fc760SBrad Bishop            if info is not None:
5402c6fc760SBrad Bishop                prop, iface = info
5412c6fc760SBrad Bishop                return prop, iface, properties_iface
5422c6fc760SBrad Bishop
5432c6fc760SBrad Bishop    def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
5442c6fc760SBrad Bishop        for i in interfaces:
5452c6fc760SBrad Bishop            properties = self.try_properties_interface(iface.GetAll, i)
54669cb6d18SBrad Bishop            if not properties:
5472c6fc760SBrad Bishop                continue
548409f671bSLeonel Gonzalez            match = obmc.utils.misc.find_case_insensitive(
549249d1327SCamVan Nguyen                prop, list(properties.keys()))
550409f671bSLeonel Gonzalez            if match is None:
5512c6fc760SBrad Bishop                continue
552409f671bSLeonel Gonzalez            prop = match
5532c6fc760SBrad Bishop            return prop, i
5542c6fc760SBrad Bishop
5552c6fc760SBrad Bishop
5562c6fc760SBrad Bishopclass SchemaHandler(RouteHandler):
5572c6fc760SBrad Bishop    verbs = ['GET']
5582c6fc760SBrad Bishop    rules = '<path:path>/schema'
5596e1ca530SDeepak Kodihalli    suppress_logging = True
5602c6fc760SBrad Bishop
5612c6fc760SBrad Bishop    def __init__(self, app, bus):
5622c6fc760SBrad Bishop        super(SchemaHandler, self).__init__(
563529029b5SBrad Bishop            app, bus, self.verbs, self.rules)
5642c6fc760SBrad Bishop
5652c6fc760SBrad Bishop    def find(self, path):
5662c6fc760SBrad Bishop        return self.try_mapper_call(
5672c6fc760SBrad Bishop            self.mapper.get_object,
5682c6fc760SBrad Bishop            path=path)
5692c6fc760SBrad Bishop
5702c6fc760SBrad Bishop    def setup(self, path):
5712c6fc760SBrad Bishop        request.route_data['map'] = self.find(path)
5722c6fc760SBrad Bishop
5732c6fc760SBrad Bishop    def do_get(self, path):
5742c6fc760SBrad Bishop        schema = {}
575249d1327SCamVan Nguyen        for x in request.route_data['map'].keys():
5762c6fc760SBrad Bishop            obj = self.bus.get_object(x, path, introspect=False)
5772c6fc760SBrad Bishop            iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
5782c6fc760SBrad Bishop            data = iface.Introspect()
5792c6fc760SBrad Bishop            parser = IntrospectionNodeParser(
5802c6fc760SBrad Bishop                ElementTree.fromstring(data))
581249d1327SCamVan Nguyen            for x, y in parser.get_interfaces().items():
5822c6fc760SBrad Bishop                schema[x] = y
5832c6fc760SBrad Bishop
5842c6fc760SBrad Bishop        return schema
5852c6fc760SBrad Bishop
5862c6fc760SBrad Bishop
5872c6fc760SBrad Bishopclass InstanceHandler(RouteHandler):
5882c6fc760SBrad Bishop    verbs = ['GET', 'PUT', 'DELETE']
5892c6fc760SBrad Bishop    rules = '<path:path>'
5902c6fc760SBrad Bishop    request_type = dict
5912c6fc760SBrad Bishop
5922c6fc760SBrad Bishop    def __init__(self, app, bus):
5932c6fc760SBrad Bishop        super(InstanceHandler, self).__init__(
594529029b5SBrad Bishop            app, bus, self.verbs, self.rules)
5952c6fc760SBrad Bishop
5962c6fc760SBrad Bishop    def find(self, path, callback=None):
5972c6fc760SBrad Bishop        return {path: self.try_mapper_call(
5982c6fc760SBrad Bishop            self.mapper.get_object,
5992c6fc760SBrad Bishop            callback,
6002c6fc760SBrad Bishop            path=path)}
6012c6fc760SBrad Bishop
6022c6fc760SBrad Bishop    def setup(self, path):
6032c6fc760SBrad Bishop        callback = None
6042c6fc760SBrad Bishop        if request.method == 'PUT':
6052c6fc760SBrad Bishop            def callback(e, **kw):
6062c6fc760SBrad Bishop                abort(403, _4034_msg % ('resource', 'created', path))
6072c6fc760SBrad Bishop
6082c6fc760SBrad Bishop        if request.route_data.get('map') is None:
6092c6fc760SBrad Bishop            request.route_data['map'] = self.find(path, callback)
6102c6fc760SBrad Bishop
6112c6fc760SBrad Bishop    def do_get(self, path):
6122c6fc760SBrad Bishop        return self.mapper.enumerate_object(
6132c6fc760SBrad Bishop            path,
6142c6fc760SBrad Bishop            mapper_data=request.route_data['map'])
6152c6fc760SBrad Bishop
6162c6fc760SBrad Bishop    def do_put(self, path):
6172c6fc760SBrad Bishop        # make sure all properties exist in the request
6182c6fc760SBrad Bishop        obj = set(self.do_get(path).keys())
6192c6fc760SBrad Bishop        req = set(request.parameter_list.keys())
6202c6fc760SBrad Bishop
6212c6fc760SBrad Bishop        diff = list(obj.difference(req))
6222c6fc760SBrad Bishop        if diff:
6232c6fc760SBrad Bishop            abort(403, _4034_msg % (
6242c6fc760SBrad Bishop                'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
6252c6fc760SBrad Bishop
6262c6fc760SBrad Bishop        diff = list(req.difference(obj))
6272c6fc760SBrad Bishop        if diff:
6282c6fc760SBrad Bishop            abort(403, _4034_msg % (
6292c6fc760SBrad Bishop                'resource', 'created', '%s/attr/%s' % (path, diff[0])))
6302c6fc760SBrad Bishop
631249d1327SCamVan Nguyen        for p, v in request.parameter_list.items():
6322c6fc760SBrad Bishop            self.app.property_handler.do_put(
6332c6fc760SBrad Bishop                path, p, v)
6342c6fc760SBrad Bishop
6352c6fc760SBrad Bishop    def do_delete(self, path):
636b1f6a2cdSMatt Spinler        deleted = False
637b1f6a2cdSMatt Spinler        for bus, interfaces in request.route_data['map'][path].items():
638b1f6a2cdSMatt Spinler            if self.bus_has_delete(interfaces):
639b1f6a2cdSMatt Spinler                self.delete_on_bus(path, bus)
640b1f6a2cdSMatt Spinler                deleted = True
641b1f6a2cdSMatt Spinler
642b1f6a2cdSMatt Spinler        #It's OK if some objects didn't have a Delete, but not all
643b1f6a2cdSMatt Spinler        if not deleted:
6442c6fc760SBrad Bishop            abort(403, _4034_msg % ('resource', 'removed', path))
6452c6fc760SBrad Bishop
646b1f6a2cdSMatt Spinler    def bus_has_delete(self, interfaces):
647b1f6a2cdSMatt Spinler        return DELETE_IFACE in interfaces
6482c6fc760SBrad Bishop
6492c6fc760SBrad Bishop    def delete_on_bus(self, path, bus):
6502c6fc760SBrad Bishop        obj = self.bus.get_object(bus, path, introspect=False)
6512c6fc760SBrad Bishop        delete_iface = dbus.Interface(
6522c6fc760SBrad Bishop            obj, dbus_interface=DELETE_IFACE)
6532c6fc760SBrad Bishop        delete_iface.Delete()
6542c6fc760SBrad Bishop
6552c6fc760SBrad Bishop
6562c6fc760SBrad Bishopclass SessionHandler(MethodHandler):
6572c6fc760SBrad Bishop    ''' Handles the /login and /logout routes, manages
6582c6fc760SBrad Bishop    server side session store and session cookies.  '''
6592c6fc760SBrad Bishop
6602c6fc760SBrad Bishop    rules = ['/login', '/logout']
6612c6fc760SBrad Bishop    login_str = "User '%s' logged %s"
6622c6fc760SBrad Bishop    bad_passwd_str = "Invalid username or password"
6632c6fc760SBrad Bishop    no_user_str = "No user logged in"
6642c6fc760SBrad Bishop    bad_json_str = "Expecting request format { 'data': " \
6652c6fc760SBrad Bishop        "[<username>, <password>] }, got '%s'"
666d08a4569SAlexander Filippov    bmc_not_ready_str = "BMC is not ready (booting)"
6672c6fc760SBrad Bishop    _require_auth = None
6682c6fc760SBrad Bishop    MAX_SESSIONS = 16
669d08a4569SAlexander Filippov    BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
670d08a4569SAlexander Filippov    BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
671d08a4569SAlexander Filippov    BMCSTATE_PROPERTY = 'CurrentBMCState'
672d08a4569SAlexander Filippov    BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
6736e1ca530SDeepak Kodihalli    suppress_json_logging = True
6742c6fc760SBrad Bishop
6752c6fc760SBrad Bishop    def __init__(self, app, bus):
6762c6fc760SBrad Bishop        super(SessionHandler, self).__init__(
6772c6fc760SBrad Bishop            app, bus)
6782c6fc760SBrad Bishop        self.hmac_key = os.urandom(128)
6792c6fc760SBrad Bishop        self.session_store = []
6802c6fc760SBrad Bishop
6812c6fc760SBrad Bishop    @staticmethod
6822c6fc760SBrad Bishop    def authenticate(username, clear):
6832c6fc760SBrad Bishop        try:
6842c6fc760SBrad Bishop            encoded = spwd.getspnam(username)[1]
6852c6fc760SBrad Bishop            return encoded == crypt.crypt(clear, encoded)
6862c6fc760SBrad Bishop        except KeyError:
6872c6fc760SBrad Bishop            return False
6882c6fc760SBrad Bishop
6892c6fc760SBrad Bishop    def invalidate_session(self, session):
6902c6fc760SBrad Bishop        try:
6912c6fc760SBrad Bishop            self.session_store.remove(session)
6922c6fc760SBrad Bishop        except ValueError:
6932c6fc760SBrad Bishop            pass
6942c6fc760SBrad Bishop
6952c6fc760SBrad Bishop    def new_session(self):
6962c6fc760SBrad Bishop        sid = os.urandom(32)
6972c6fc760SBrad Bishop        if self.MAX_SESSIONS <= len(self.session_store):
6982c6fc760SBrad Bishop            self.session_store.pop()
6992c6fc760SBrad Bishop        self.session_store.insert(0, {'sid': sid})
7002c6fc760SBrad Bishop
7012c6fc760SBrad Bishop        return self.session_store[0]
7022c6fc760SBrad Bishop
7032c6fc760SBrad Bishop    def get_session(self, sid):
7042c6fc760SBrad Bishop        sids = [x['sid'] for x in self.session_store]
7052c6fc760SBrad Bishop        try:
7062c6fc760SBrad Bishop            return self.session_store[sids.index(sid)]
7072c6fc760SBrad Bishop        except ValueError:
7082c6fc760SBrad Bishop            return None
7092c6fc760SBrad Bishop
7102c6fc760SBrad Bishop    def get_session_from_cookie(self):
7112c6fc760SBrad Bishop        return self.get_session(
7122c6fc760SBrad Bishop            request.get_cookie(
7132c6fc760SBrad Bishop                'sid', secret=self.hmac_key))
7142c6fc760SBrad Bishop
7152c6fc760SBrad Bishop    def do_post(self, **kw):
7162c6fc760SBrad Bishop        if request.path == '/login':
7172c6fc760SBrad Bishop            return self.do_login(**kw)
7182c6fc760SBrad Bishop        else:
7192c6fc760SBrad Bishop            return self.do_logout(**kw)
7202c6fc760SBrad Bishop
7212c6fc760SBrad Bishop    def do_logout(self, **kw):
7222c6fc760SBrad Bishop        session = self.get_session_from_cookie()
7232c6fc760SBrad Bishop        if session is not None:
7242c6fc760SBrad Bishop            user = session['user']
7252c6fc760SBrad Bishop            self.invalidate_session(session)
7262c6fc760SBrad Bishop            response.delete_cookie('sid')
7272c6fc760SBrad Bishop            return self.login_str % (user, 'out')
7282c6fc760SBrad Bishop
7292c6fc760SBrad Bishop        return self.no_user_str
7302c6fc760SBrad Bishop
7312c6fc760SBrad Bishop    def do_login(self, **kw):
7322c6fc760SBrad Bishop        if len(request.parameter_list) != 2:
7332c6fc760SBrad Bishop            abort(400, self.bad_json_str % (request.json))
7342c6fc760SBrad Bishop
7352c6fc760SBrad Bishop        if not self.authenticate(*request.parameter_list):
736dc3fbfa7SBrad Bishop            abort(401, self.bad_passwd_str)
7372c6fc760SBrad Bishop
738d08a4569SAlexander Filippov        force = False
739d08a4569SAlexander Filippov        try:
740d08a4569SAlexander Filippov            force = request.json.get('force')
741d08a4569SAlexander Filippov        except (ValueError, AttributeError, KeyError, TypeError):
742d08a4569SAlexander Filippov            force = False
743d08a4569SAlexander Filippov
744d08a4569SAlexander Filippov        if not force and not self.is_bmc_ready():
745d08a4569SAlexander Filippov            abort(503, self.bmc_not_ready_str)
746d08a4569SAlexander Filippov
7472c6fc760SBrad Bishop        user = request.parameter_list[0]
7482c6fc760SBrad Bishop        session = self.new_session()
7492c6fc760SBrad Bishop        session['user'] = user
7502c6fc760SBrad Bishop        response.set_cookie(
7512c6fc760SBrad Bishop            'sid', session['sid'], secret=self.hmac_key,
7522c6fc760SBrad Bishop            secure=True,
7532c6fc760SBrad Bishop            httponly=True)
7542c6fc760SBrad Bishop        return self.login_str % (user, 'in')
7552c6fc760SBrad Bishop
756d08a4569SAlexander Filippov    def is_bmc_ready(self):
757d08a4569SAlexander Filippov        if not self.app.with_bmc_check:
758d08a4569SAlexander Filippov            return True
759d08a4569SAlexander Filippov
760d08a4569SAlexander Filippov        try:
761d08a4569SAlexander Filippov            obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
762d08a4569SAlexander Filippov            iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
763d08a4569SAlexander Filippov            state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
764d08a4569SAlexander Filippov            if state == self.BMCSTATE_READY:
765d08a4569SAlexander Filippov                return True
766d08a4569SAlexander Filippov
767d08a4569SAlexander Filippov        except dbus.exceptions.DBusException:
768d08a4569SAlexander Filippov            pass
769d08a4569SAlexander Filippov
770d08a4569SAlexander Filippov        return False
771d08a4569SAlexander Filippov
7722c6fc760SBrad Bishop    def find(self, **kw):
7732c6fc760SBrad Bishop        pass
7742c6fc760SBrad Bishop
7752c6fc760SBrad Bishop    def setup(self, **kw):
7762c6fc760SBrad Bishop        pass
7772c6fc760SBrad Bishop
7782c6fc760SBrad Bishop
7797ec0a4f4SDeepak Kodihalliclass ImageUploadUtils:
7807ec0a4f4SDeepak Kodihalli    ''' Provides common utils for image upload. '''
7811af301a0SDeepak Kodihalli
7821af301a0SDeepak Kodihalli    file_loc = '/tmp/images'
7831af301a0SDeepak Kodihalli    file_prefix = 'img'
7841af301a0SDeepak Kodihalli    file_suffix = ''
78553693891SAdriana Kobylak    signal = None
7861af301a0SDeepak Kodihalli
7877ec0a4f4SDeepak Kodihalli    @classmethod
7887ec0a4f4SDeepak Kodihalli    def do_upload(cls, filename=''):
78953693891SAdriana Kobylak        def cleanup():
79053693891SAdriana Kobylak            os.close(handle)
79153693891SAdriana Kobylak            if cls.signal:
79253693891SAdriana Kobylak                cls.signal.remove()
79353693891SAdriana Kobylak                cls.signal = None
79453693891SAdriana Kobylak
79553693891SAdriana Kobylak        def signal_callback(path, a, **kw):
79653693891SAdriana Kobylak            # Just interested on the first Version interface created which is
79753693891SAdriana Kobylak            # triggered when the file is uploaded. This helps avoid getting the
79853693891SAdriana Kobylak            # wrong information for multiple upload requests in a row.
79953693891SAdriana Kobylak            if "xyz.openbmc_project.Software.Version" in a and \
80053693891SAdriana Kobylak               "xyz.openbmc_project.Software.Activation" not in a:
80153693891SAdriana Kobylak                paths.append(path)
80253693891SAdriana Kobylak
80353693891SAdriana Kobylak        while cls.signal:
80453693891SAdriana Kobylak            # Serialize uploads by waiting for the signal to be cleared.
80553693891SAdriana Kobylak            # This makes it easier to ensure that the version information
80653693891SAdriana Kobylak            # is the right one instead of the data from another upload request.
80753693891SAdriana Kobylak            gevent.sleep(1)
8087ec0a4f4SDeepak Kodihalli        if not os.path.exists(cls.file_loc):
809fb515796SGunnar Mills            abort(500, "Error Directory not found")
81053693891SAdriana Kobylak        paths = []
81153693891SAdriana Kobylak        bus = dbus.SystemBus()
81253693891SAdriana Kobylak        cls.signal = bus.add_signal_receiver(
81353693891SAdriana Kobylak            signal_callback,
81453693891SAdriana Kobylak            dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
81553693891SAdriana Kobylak            signal_name='InterfacesAdded',
81653693891SAdriana Kobylak            path=SOFTWARE_PATH)
8177ec0a4f4SDeepak Kodihalli        if not filename:
8187ec0a4f4SDeepak Kodihalli            handle, filename = tempfile.mkstemp(cls.file_suffix,
8197ec0a4f4SDeepak Kodihalli                                                cls.file_prefix, cls.file_loc)
8207ec0a4f4SDeepak Kodihalli        else:
8217ec0a4f4SDeepak Kodihalli            filename = os.path.join(cls.file_loc, filename)
822b66b18cbSGunnar Mills            handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
8230b62edfbSLeonel Gonzalez        try:
8240b62edfbSLeonel Gonzalez            file_contents = request.body.read()
8250b62edfbSLeonel Gonzalez            request.body.close()
826b66b18cbSGunnar Mills            os.write(handle, file_contents)
82753693891SAdriana Kobylak            # Close file after writing, the image manager process watches for
82853693891SAdriana Kobylak            # the close event to know the upload is complete.
829b66b18cbSGunnar Mills            os.close(handle)
83053693891SAdriana Kobylak        except (IOError, ValueError) as e:
83153693891SAdriana Kobylak            cleanup()
83253693891SAdriana Kobylak            abort(400, str(e))
83353693891SAdriana Kobylak        except Exception:
83453693891SAdriana Kobylak            cleanup()
83553693891SAdriana Kobylak            abort(400, "Unexpected Error")
83653693891SAdriana Kobylak        loop = gobject.MainLoop()
83753693891SAdriana Kobylak        gcontext = loop.get_context()
83853693891SAdriana Kobylak        count = 0
83953693891SAdriana Kobylak        version_id = ''
84053693891SAdriana Kobylak        while loop is not None:
84153693891SAdriana Kobylak            try:
84253693891SAdriana Kobylak                if gcontext.pending():
84353693891SAdriana Kobylak                    gcontext.iteration()
84453693891SAdriana Kobylak                if not paths:
84553693891SAdriana Kobylak                    gevent.sleep(1)
84653693891SAdriana Kobylak                else:
84753693891SAdriana Kobylak                    version_id = os.path.basename(paths.pop())
84853693891SAdriana Kobylak                    break
84953693891SAdriana Kobylak                count += 1
85053693891SAdriana Kobylak                if count == 10:
85153693891SAdriana Kobylak                    break
85253693891SAdriana Kobylak            except Exception:
85353693891SAdriana Kobylak                break
85453693891SAdriana Kobylak        cls.signal.remove()
85553693891SAdriana Kobylak        cls.signal = None
85697fe435fSAdriana Kobylak        if version_id:
85753693891SAdriana Kobylak            return version_id
85897fe435fSAdriana Kobylak        else:
85997fe435fSAdriana Kobylak            abort(400, "Version already exists or failed to be extracted")
8607ec0a4f4SDeepak Kodihalli
8617ec0a4f4SDeepak Kodihalli
8627ec0a4f4SDeepak Kodihalliclass ImagePostHandler(RouteHandler):
8637ec0a4f4SDeepak Kodihalli    ''' Handles the /upload/image route. '''
8647ec0a4f4SDeepak Kodihalli
8657ec0a4f4SDeepak Kodihalli    verbs = ['POST']
8667ec0a4f4SDeepak Kodihalli    rules = ['/upload/image']
8677ec0a4f4SDeepak Kodihalli    content_type = 'application/octet-stream'
8687ec0a4f4SDeepak Kodihalli
8691af301a0SDeepak Kodihalli    def __init__(self, app, bus):
8707ec0a4f4SDeepak Kodihalli        super(ImagePostHandler, self).__init__(
8711af301a0SDeepak Kodihalli            app, bus, self.verbs, self.rules, self.content_type)
8721af301a0SDeepak Kodihalli
8737ec0a4f4SDeepak Kodihalli    def do_post(self, filename=''):
87453693891SAdriana Kobylak        return ImageUploadUtils.do_upload()
8751af301a0SDeepak Kodihalli
8767ec0a4f4SDeepak Kodihalli    def find(self, **kw):
8777ec0a4f4SDeepak Kodihalli        pass
8781af301a0SDeepak Kodihalli
8797ec0a4f4SDeepak Kodihalli    def setup(self, **kw):
8807ec0a4f4SDeepak Kodihalli        pass
8817ec0a4f4SDeepak Kodihalli
8827ec0a4f4SDeepak Kodihalli
883dee2ef57SDhruvaraj Subhashchandranclass CertificateHandler:
884dee2ef57SDhruvaraj Subhashchandran    file_suffix = '.pem'
885dee2ef57SDhruvaraj Subhashchandran    file_prefix = 'cert_'
886dee2ef57SDhruvaraj Subhashchandran    CERT_PATH = '/xyz/openbmc_project/certs'
887dee2ef57SDhruvaraj Subhashchandran    CERT_IFACE = 'xyz.openbmc_project.Certs.Install'
888dee2ef57SDhruvaraj Subhashchandran
889a324acd9SDeepak Kodihalli    def __init__(self, route_handler, cert_type, service):
890dee2ef57SDhruvaraj Subhashchandran        if not service:
891dee2ef57SDhruvaraj Subhashchandran            abort(500, "Missing service")
892dee2ef57SDhruvaraj Subhashchandran        if not cert_type:
893dee2ef57SDhruvaraj Subhashchandran            abort(500, "Missing certificate type")
894a324acd9SDeepak Kodihalli        bus = dbus.SystemBus()
895a324acd9SDeepak Kodihalli        certPath = self.CERT_PATH + "/" + cert_type + "/" + service
896a324acd9SDeepak Kodihalli        intfs = route_handler.try_mapper_call(
897a324acd9SDeepak Kodihalli            route_handler.mapper.get_object, path=certPath)
898a324acd9SDeepak Kodihalli        for busName,intf in intfs.items():
899a324acd9SDeepak Kodihalli            if self.CERT_IFACE in intf:
900a324acd9SDeepak Kodihalli                self.obj = bus.get_object(busName, certPath)
901a324acd9SDeepak Kodihalli                return
902a324acd9SDeepak Kodihalli        abort(404, "Path not found")
903a324acd9SDeepak Kodihalli
904a324acd9SDeepak Kodihalli    def do_upload(self):
905a324acd9SDeepak Kodihalli        def cleanup():
906a324acd9SDeepak Kodihalli            if os.path.exists(temp.name):
907a324acd9SDeepak Kodihalli                os.remove(temp.name)
908dee2ef57SDhruvaraj Subhashchandran
909dee2ef57SDhruvaraj Subhashchandran        with tempfile.NamedTemporaryFile(
910a324acd9SDeepak Kodihalli            suffix=self.file_suffix,
911a324acd9SDeepak Kodihalli            prefix=self.file_prefix,
912dee2ef57SDhruvaraj Subhashchandran            delete=False) as temp:
913dee2ef57SDhruvaraj Subhashchandran            try:
914dee2ef57SDhruvaraj Subhashchandran                file_contents = request.body.read()
915dee2ef57SDhruvaraj Subhashchandran                request.body.close()
916dee2ef57SDhruvaraj Subhashchandran                temp.write(file_contents)
917dee2ef57SDhruvaraj Subhashchandran            except (IOError, ValueError) as e:
918dee2ef57SDhruvaraj Subhashchandran                cleanup()
919dee2ef57SDhruvaraj Subhashchandran                abort(500, str(e))
920dee2ef57SDhruvaraj Subhashchandran            except Exception:
921dee2ef57SDhruvaraj Subhashchandran                cleanup()
922dee2ef57SDhruvaraj Subhashchandran                abort(500, "Unexpected Error")
923dee2ef57SDhruvaraj Subhashchandran
924dee2ef57SDhruvaraj Subhashchandran        try:
925a324acd9SDeepak Kodihalli            iface = dbus.Interface(self.obj, self.CERT_IFACE)
926dee2ef57SDhruvaraj Subhashchandran            iface.Install(temp.name)
927c043cddaSDeepak Kodihalli        except Exception as e:
928dee2ef57SDhruvaraj Subhashchandran            cleanup()
929844bb4e1SDeepak Kodihalli            abort(400, str(e))
930a324acd9SDeepak Kodihalli        cleanup()
931a324acd9SDeepak Kodihalli
932a324acd9SDeepak Kodihalli    def do_delete(self):
933a324acd9SDeepak Kodihalli        delete_iface = dbus.Interface(
934a324acd9SDeepak Kodihalli            self.obj, dbus_interface=DELETE_IFACE)
935a324acd9SDeepak Kodihalli        delete_iface.Delete()
936dee2ef57SDhruvaraj Subhashchandran
937dee2ef57SDhruvaraj Subhashchandran
938dee2ef57SDhruvaraj Subhashchandranclass CertificatePutHandler(RouteHandler):
939dee2ef57SDhruvaraj Subhashchandran    ''' Handles the /xyz/openbmc_project/certs/<cert_type>/<service> route. '''
940dee2ef57SDhruvaraj Subhashchandran
941a324acd9SDeepak Kodihalli    verbs = ['PUT', 'DELETE']
942dee2ef57SDhruvaraj Subhashchandran    rules = ['/xyz/openbmc_project/certs/<cert_type>/<service>']
943dee2ef57SDhruvaraj Subhashchandran    content_type = 'application/octet-stream'
944dee2ef57SDhruvaraj Subhashchandran
945dee2ef57SDhruvaraj Subhashchandran    def __init__(self, app, bus):
946dee2ef57SDhruvaraj Subhashchandran        super(CertificatePutHandler, self).__init__(
947dee2ef57SDhruvaraj Subhashchandran            app, bus, self.verbs, self.rules, self.content_type)
948dee2ef57SDhruvaraj Subhashchandran
949dee2ef57SDhruvaraj Subhashchandran    def do_put(self, cert_type, service):
950a324acd9SDeepak Kodihalli        return CertificateHandler(self, cert_type, service).do_upload()
951a324acd9SDeepak Kodihalli
952a324acd9SDeepak Kodihalli    def do_delete(self, cert_type, service):
953a324acd9SDeepak Kodihalli        return CertificateHandler(self, cert_type, service).do_delete()
954dee2ef57SDhruvaraj Subhashchandran
955dee2ef57SDhruvaraj Subhashchandran    def find(self, **kw):
956dee2ef57SDhruvaraj Subhashchandran        pass
957dee2ef57SDhruvaraj Subhashchandran
958dee2ef57SDhruvaraj Subhashchandran    def setup(self, **kw):
959dee2ef57SDhruvaraj Subhashchandran        pass
960dee2ef57SDhruvaraj Subhashchandran
961dee2ef57SDhruvaraj Subhashchandran
962639b502eSDeepak Kodihalliclass EventNotifier:
963639b502eSDeepak Kodihalli    keyNames = {}
964639b502eSDeepak Kodihalli    keyNames['event'] = 'event'
965639b502eSDeepak Kodihalli    keyNames['path'] = 'path'
966639b502eSDeepak Kodihalli    keyNames['intfMap'] = 'interfaces'
967639b502eSDeepak Kodihalli    keyNames['propMap'] = 'properties'
968639b502eSDeepak Kodihalli    keyNames['intf'] = 'interface'
969639b502eSDeepak Kodihalli
970639b502eSDeepak Kodihalli    def __init__(self, wsock, filters):
971639b502eSDeepak Kodihalli        self.wsock = wsock
972639b502eSDeepak Kodihalli        self.paths = filters.get("paths", [])
973639b502eSDeepak Kodihalli        self.interfaces = filters.get("interfaces", [])
9740f7019dfSAndrew Geissler        self.signals = []
9750f7019dfSAndrew Geissler        self.socket_error = False
976639b502eSDeepak Kodihalli        if not self.paths:
977639b502eSDeepak Kodihalli            self.paths.append(None)
978639b502eSDeepak Kodihalli        bus = dbus.SystemBus()
979639b502eSDeepak Kodihalli        # Add a signal receiver for every path the client is interested in
980639b502eSDeepak Kodihalli        for path in self.paths:
9810f7019dfSAndrew Geissler            add_sig = bus.add_signal_receiver(
982639b502eSDeepak Kodihalli                self.interfaces_added_handler,
983639b502eSDeepak Kodihalli                dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
984639b502eSDeepak Kodihalli                signal_name='InterfacesAdded',
985639b502eSDeepak Kodihalli                path=path)
9860f7019dfSAndrew Geissler            chg_sig = bus.add_signal_receiver(
987639b502eSDeepak Kodihalli                self.properties_changed_handler,
988639b502eSDeepak Kodihalli                dbus_interface=dbus.PROPERTIES_IFACE,
989639b502eSDeepak Kodihalli                signal_name='PropertiesChanged',
990639b502eSDeepak Kodihalli                path=path,
991639b502eSDeepak Kodihalli                path_keyword='path')
9920f7019dfSAndrew Geissler            self.signals.append(add_sig)
9930f7019dfSAndrew Geissler            self.signals.append(chg_sig)
994639b502eSDeepak Kodihalli        loop = gobject.MainLoop()
995639b502eSDeepak Kodihalli        # gobject's mainloop.run() will block the entire process, so the gevent
996639b502eSDeepak Kodihalli        # scheduler and hence greenlets won't execute. The while-loop below
997639b502eSDeepak Kodihalli        # works around this limitation by using gevent's sleep, instead of
998639b502eSDeepak Kodihalli        # calling loop.run()
999639b502eSDeepak Kodihalli        gcontext = loop.get_context()
1000639b502eSDeepak Kodihalli        while loop is not None:
1001639b502eSDeepak Kodihalli            try:
10020f7019dfSAndrew Geissler                if self.socket_error:
10030f7019dfSAndrew Geissler                    for signal in self.signals:
10040f7019dfSAndrew Geissler                        signal.remove()
10050f7019dfSAndrew Geissler                    loop.quit()
10060f7019dfSAndrew Geissler                    break;
1007639b502eSDeepak Kodihalli                if gcontext.pending():
1008639b502eSDeepak Kodihalli                    gcontext.iteration()
1009639b502eSDeepak Kodihalli                else:
1010639b502eSDeepak Kodihalli                    # gevent.sleep puts only the current greenlet to sleep,
1011639b502eSDeepak Kodihalli                    # not the entire process.
1012639b502eSDeepak Kodihalli                    gevent.sleep(5)
1013639b502eSDeepak Kodihalli            except WebSocketError:
1014639b502eSDeepak Kodihalli                break
1015639b502eSDeepak Kodihalli
1016639b502eSDeepak Kodihalli    def interfaces_added_handler(self, path, iprops, **kw):
1017639b502eSDeepak Kodihalli        ''' If the client is interested in these changes, respond to the
1018639b502eSDeepak Kodihalli            client. This handles d-bus interface additions.'''
1019639b502eSDeepak Kodihalli        if (not self.interfaces) or \
1020639b502eSDeepak Kodihalli           (not set(iprops).isdisjoint(self.interfaces)):
1021639b502eSDeepak Kodihalli            response = {}
1022639b502eSDeepak Kodihalli            response[self.keyNames['event']] = "InterfacesAdded"
1023639b502eSDeepak Kodihalli            response[self.keyNames['path']] = path
1024639b502eSDeepak Kodihalli            response[self.keyNames['intfMap']] = iprops
1025639b502eSDeepak Kodihalli            try:
1026639b502eSDeepak Kodihalli                self.wsock.send(json.dumps(response))
10270f7019dfSAndrew Geissler            except:
10280f7019dfSAndrew Geissler                self.socket_error = True
1029639b502eSDeepak Kodihalli                return
1030639b502eSDeepak Kodihalli
1031639b502eSDeepak Kodihalli    def properties_changed_handler(self, interface, new, old, **kw):
1032639b502eSDeepak Kodihalli        ''' If the client is interested in these changes, respond to the
1033639b502eSDeepak Kodihalli            client. This handles d-bus property changes. '''
1034639b502eSDeepak Kodihalli        if (not self.interfaces) or (interface in self.interfaces):
1035639b502eSDeepak Kodihalli            path = str(kw['path'])
1036639b502eSDeepak Kodihalli            response = {}
1037639b502eSDeepak Kodihalli            response[self.keyNames['event']] = "PropertiesChanged"
1038639b502eSDeepak Kodihalli            response[self.keyNames['path']] = path
1039639b502eSDeepak Kodihalli            response[self.keyNames['intf']] = interface
1040639b502eSDeepak Kodihalli            response[self.keyNames['propMap']] = new
1041639b502eSDeepak Kodihalli            try:
1042639b502eSDeepak Kodihalli                self.wsock.send(json.dumps(response))
10430f7019dfSAndrew Geissler            except:
10440f7019dfSAndrew Geissler                self.socket_error = True
1045639b502eSDeepak Kodihalli                return
1046639b502eSDeepak Kodihalli
1047639b502eSDeepak Kodihalli
1048b209dd16SDeepak Kodihalliclass EventHandler(RouteHandler):
1049b209dd16SDeepak Kodihalli    ''' Handles the /subscribe route, for clients to be able
1050b209dd16SDeepak Kodihalli        to subscribe to BMC events. '''
1051b209dd16SDeepak Kodihalli
1052b209dd16SDeepak Kodihalli    verbs = ['GET']
1053b209dd16SDeepak Kodihalli    rules = ['/subscribe']
10546e1ca530SDeepak Kodihalli    suppress_logging = True
1055b209dd16SDeepak Kodihalli
1056b209dd16SDeepak Kodihalli    def __init__(self, app, bus):
1057b209dd16SDeepak Kodihalli        super(EventHandler, self).__init__(
1058b209dd16SDeepak Kodihalli            app, bus, self.verbs, self.rules)
1059b209dd16SDeepak Kodihalli
1060b209dd16SDeepak Kodihalli    def find(self, **kw):
1061b209dd16SDeepak Kodihalli        pass
1062b209dd16SDeepak Kodihalli
1063b209dd16SDeepak Kodihalli    def setup(self, **kw):
1064b209dd16SDeepak Kodihalli        pass
1065b209dd16SDeepak Kodihalli
1066b209dd16SDeepak Kodihalli    def do_get(self):
1067b209dd16SDeepak Kodihalli        wsock = request.environ.get('wsgi.websocket')
1068b209dd16SDeepak Kodihalli        if not wsock:
1069b209dd16SDeepak Kodihalli            abort(400, 'Expected WebSocket request.')
1070bec10c20SJayashankar Padath        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
1071639b502eSDeepak Kodihalli        filters = wsock.receive()
1072639b502eSDeepak Kodihalli        filters = json.loads(filters)
1073639b502eSDeepak Kodihalli        notifier = EventNotifier(wsock, filters)
1074b209dd16SDeepak Kodihalli
10755c518f63SDeepak Kodihalliclass HostConsoleHandler(RouteHandler):
10765c518f63SDeepak Kodihalli    ''' Handles the /console route, for clients to be able
10775c518f63SDeepak Kodihalli        read/write the host serial console. The way this is
10785c518f63SDeepak Kodihalli        done is by exposing a websocket that's mirrored to an
10795c518f63SDeepak Kodihalli        abstract UNIX domain socket, which is the source for
10805c518f63SDeepak Kodihalli        the console data. '''
10815c518f63SDeepak Kodihalli
10825c518f63SDeepak Kodihalli    verbs = ['GET']
10835c518f63SDeepak Kodihalli    # Naming the route console0, because the numbering will help
10845c518f63SDeepak Kodihalli    # on multi-bmc/multi-host systems.
10855c518f63SDeepak Kodihalli    rules = ['/console0']
10866e1ca530SDeepak Kodihalli    suppress_logging = True
10875c518f63SDeepak Kodihalli
10885c518f63SDeepak Kodihalli    def __init__(self, app, bus):
10895c518f63SDeepak Kodihalli        super(HostConsoleHandler, self).__init__(
10905c518f63SDeepak Kodihalli            app, bus, self.verbs, self.rules)
10915c518f63SDeepak Kodihalli
10925c518f63SDeepak Kodihalli    def find(self, **kw):
10935c518f63SDeepak Kodihalli        pass
10945c518f63SDeepak Kodihalli
10955c518f63SDeepak Kodihalli    def setup(self, **kw):
10965c518f63SDeepak Kodihalli        pass
10975c518f63SDeepak Kodihalli
10985c518f63SDeepak Kodihalli    def read_wsock(self, wsock, sock):
10995c518f63SDeepak Kodihalli        while True:
11005c518f63SDeepak Kodihalli            try:
11015c518f63SDeepak Kodihalli                incoming = wsock.receive()
11025c518f63SDeepak Kodihalli                if incoming:
11035c518f63SDeepak Kodihalli                    # Read websocket, write to UNIX socket
11045c518f63SDeepak Kodihalli                    sock.send(incoming)
11055c518f63SDeepak Kodihalli            except Exception as e:
11065c518f63SDeepak Kodihalli                sock.close()
11075c518f63SDeepak Kodihalli                return
11085c518f63SDeepak Kodihalli
11095c518f63SDeepak Kodihalli    def read_sock(self, sock, wsock):
11105c518f63SDeepak Kodihalli        max_sock_read_len = 4096
11115c518f63SDeepak Kodihalli        while True:
11125c518f63SDeepak Kodihalli            try:
11135c518f63SDeepak Kodihalli                outgoing = sock.recv(max_sock_read_len)
11145c518f63SDeepak Kodihalli                if outgoing:
11155c518f63SDeepak Kodihalli                    # Read UNIX socket, write to websocket
11165c518f63SDeepak Kodihalli                    wsock.send(outgoing)
11175c518f63SDeepak Kodihalli            except Exception as e:
11185c518f63SDeepak Kodihalli                wsock.close()
11195c518f63SDeepak Kodihalli                return
11205c518f63SDeepak Kodihalli
11215c518f63SDeepak Kodihalli    def do_get(self):
11225c518f63SDeepak Kodihalli        wsock = request.environ.get('wsgi.websocket')
11235c518f63SDeepak Kodihalli        if not wsock:
11245c518f63SDeepak Kodihalli            abort(400, 'Expected WebSocket based request.')
11255c518f63SDeepak Kodihalli
1126dbc46919SVernon Mauery        # An abstract Unix socket path must be less than or equal to 108 bytes
1127dbc46919SVernon Mauery        # and does not need to be nul-terminated or padded out to 108 bytes
11285c518f63SDeepak Kodihalli        socket_name = "\0obmc-console"
1129dbc46919SVernon Mauery        socket_path = socket_name
11305c518f63SDeepak Kodihalli        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
11315c518f63SDeepak Kodihalli
11325c518f63SDeepak Kodihalli        try:
11335c518f63SDeepak Kodihalli            sock.connect(socket_path)
11345c518f63SDeepak Kodihalli        except Exception as e:
11355c518f63SDeepak Kodihalli            abort(500, str(e))
11365c518f63SDeepak Kodihalli
11375c518f63SDeepak Kodihalli        wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
11385c518f63SDeepak Kodihalli        sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
1139bec10c20SJayashankar Padath        ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
11405c518f63SDeepak Kodihalli        gevent.joinall([wsock_reader, sock_reader, ping_sender])
11415c518f63SDeepak Kodihalli
11425c518f63SDeepak Kodihalli
11437ec0a4f4SDeepak Kodihalliclass ImagePutHandler(RouteHandler):
11447ec0a4f4SDeepak Kodihalli    ''' Handles the /upload/image/<filename> route. '''
11457ec0a4f4SDeepak Kodihalli
11467ec0a4f4SDeepak Kodihalli    verbs = ['PUT']
11477ec0a4f4SDeepak Kodihalli    rules = ['/upload/image/<filename>']
11487ec0a4f4SDeepak Kodihalli    content_type = 'application/octet-stream'
11497ec0a4f4SDeepak Kodihalli
11507ec0a4f4SDeepak Kodihalli    def __init__(self, app, bus):
11517ec0a4f4SDeepak Kodihalli        super(ImagePutHandler, self).__init__(
11527ec0a4f4SDeepak Kodihalli            app, bus, self.verbs, self.rules, self.content_type)
11537ec0a4f4SDeepak Kodihalli
11547ec0a4f4SDeepak Kodihalli    def do_put(self, filename=''):
115553693891SAdriana Kobylak        return ImageUploadUtils.do_upload(filename)
11561af301a0SDeepak Kodihalli
11571af301a0SDeepak Kodihalli    def find(self, **kw):
11581af301a0SDeepak Kodihalli        pass
11591af301a0SDeepak Kodihalli
11601af301a0SDeepak Kodihalli    def setup(self, **kw):
11611af301a0SDeepak Kodihalli        pass
11621af301a0SDeepak Kodihalli
11631af301a0SDeepak Kodihalli
11649bc94997SJayanth Othayothclass DownloadDumpHandler(RouteHandler):
11659bc94997SJayanth Othayoth    ''' Handles the /download/dump route. '''
11669bc94997SJayanth Othayoth
11679bc94997SJayanth Othayoth    verbs = 'GET'
11689bc94997SJayanth Othayoth    rules = ['/download/dump/<dumpid>']
11699bc94997SJayanth Othayoth    content_type = 'application/octet-stream'
117018c3a242SJayanth Othayoth    dump_loc = '/var/lib/phosphor-debug-collector/dumps'
1171944cd04fSBrad Bishop    suppress_json_resp = True
11726e1ca530SDeepak Kodihalli    suppress_logging = True
11739bc94997SJayanth Othayoth
11749bc94997SJayanth Othayoth    def __init__(self, app, bus):
11759bc94997SJayanth Othayoth        super(DownloadDumpHandler, self).__init__(
11769bc94997SJayanth Othayoth            app, bus, self.verbs, self.rules, self.content_type)
11779bc94997SJayanth Othayoth
11789bc94997SJayanth Othayoth    def do_get(self, dumpid):
11799bc94997SJayanth Othayoth        return self.do_download(dumpid)
11809bc94997SJayanth Othayoth
11819bc94997SJayanth Othayoth    def find(self, **kw):
11829bc94997SJayanth Othayoth        pass
11839bc94997SJayanth Othayoth
11849bc94997SJayanth Othayoth    def setup(self, **kw):
11859bc94997SJayanth Othayoth        pass
11869bc94997SJayanth Othayoth
11879bc94997SJayanth Othayoth    def do_download(self, dumpid):
11889bc94997SJayanth Othayoth        dump_loc = os.path.join(self.dump_loc, dumpid)
11899bc94997SJayanth Othayoth        if not os.path.exists(dump_loc):
11909bc94997SJayanth Othayoth            abort(404, "Path not found")
11919bc94997SJayanth Othayoth
11929bc94997SJayanth Othayoth        files = os.listdir(dump_loc)
11939bc94997SJayanth Othayoth        num_files = len(files)
11949bc94997SJayanth Othayoth        if num_files == 0:
11959bc94997SJayanth Othayoth            abort(404, "Dump not found")
11969bc94997SJayanth Othayoth
11979bc94997SJayanth Othayoth        return static_file(os.path.basename(files[0]), root=dump_loc,
11989bc94997SJayanth Othayoth                           download=True, mimetype=self.content_type)
11999bc94997SJayanth Othayoth
12009bc94997SJayanth Othayoth
1201d41643ecSMatt Spinlerclass WebHandler(RouteHandler):
1202d41643ecSMatt Spinler    ''' Handles the routes for the web UI files. '''
1203d41643ecSMatt Spinler
1204d41643ecSMatt Spinler    verbs = 'GET'
1205d41643ecSMatt Spinler
1206d41643ecSMatt Spinler    # Match only what we know are web files, so everything else
1207d41643ecSMatt Spinler    # can get routed to the REST handlers.
1208d41643ecSMatt Spinler    rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1209d41643ecSMatt Spinler             '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1210d41643ecSMatt Spinler             '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1211d41643ecSMatt Spinler             '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1212d41643ecSMatt Spinler             '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1213d41643ecSMatt Spinler             '/<filename:re:.+\.ico>']
1214d41643ecSMatt Spinler
1215d41643ecSMatt Spinler    # The mimetypes module knows about most types, but not these
1216d41643ecSMatt Spinler    content_types = {
1217d41643ecSMatt Spinler        '.eot': 'application/vnd.ms-fontobject',
1218d41643ecSMatt Spinler        '.woff': 'application/x-font-woff',
1219d41643ecSMatt Spinler        '.woff2': 'application/x-font-woff2',
1220d41643ecSMatt Spinler        '.ttf': 'application/x-font-ttf',
1221d41643ecSMatt Spinler        '.map': 'application/json'
1222d41643ecSMatt Spinler    }
1223d41643ecSMatt Spinler
1224d41643ecSMatt Spinler    _require_auth = None
1225d41643ecSMatt Spinler    suppress_json_resp = True
12266e1ca530SDeepak Kodihalli    suppress_logging = True
1227d41643ecSMatt Spinler
1228d41643ecSMatt Spinler    def __init__(self, app, bus):
1229d41643ecSMatt Spinler        super(WebHandler, self).__init__(
1230d41643ecSMatt Spinler            app, bus, self.verbs, self.rules)
1231d41643ecSMatt Spinler
1232d41643ecSMatt Spinler    def get_type(self, filename):
1233d41643ecSMatt Spinler        ''' Returns the content type and encoding for a file '''
1234d41643ecSMatt Spinler
1235d41643ecSMatt Spinler        content_type, encoding = mimetypes.guess_type(filename)
1236d41643ecSMatt Spinler
1237d41643ecSMatt Spinler        # Try our own list if mimetypes didn't recognize it
1238d41643ecSMatt Spinler        if content_type is None:
1239d41643ecSMatt Spinler            if filename[-3:] == '.gz':
1240d41643ecSMatt Spinler                filename = filename[:-3]
1241d41643ecSMatt Spinler            extension = filename[filename.rfind('.'):]
1242d41643ecSMatt Spinler            content_type = self.content_types.get(extension, None)
1243d41643ecSMatt Spinler
1244d41643ecSMatt Spinler        return content_type, encoding
1245d41643ecSMatt Spinler
1246d41643ecSMatt Spinler    def do_get(self, filename='index.html'):
1247d41643ecSMatt Spinler
1248d41643ecSMatt Spinler        # If a gzipped version exists, use that instead.
1249d41643ecSMatt Spinler        # Possible future enhancement: if the client doesn't
1250d41643ecSMatt Spinler        # accept compressed files, unzip it ourselves before sending.
1251d41643ecSMatt Spinler        if not os.path.exists(os.path.join(www_base_path, filename)):
1252d41643ecSMatt Spinler            filename = filename + '.gz'
1253d41643ecSMatt Spinler
1254d41643ecSMatt Spinler        # Though bottle should protect us, ensure path is valid
1255d41643ecSMatt Spinler        realpath = os.path.realpath(filename)
1256d41643ecSMatt Spinler        if realpath[0] == '/':
1257d41643ecSMatt Spinler            realpath = realpath[1:]
1258d41643ecSMatt Spinler        if not os.path.exists(os.path.join(www_base_path, realpath)):
1259d41643ecSMatt Spinler            abort(404, "Path not found")
1260d41643ecSMatt Spinler
1261d41643ecSMatt Spinler        mimetype, encoding = self.get_type(filename)
1262d41643ecSMatt Spinler
1263d41643ecSMatt Spinler        # Couldn't find the type - let static_file() deal with it,
1264d41643ecSMatt Spinler        # though this should never happen.
1265d41643ecSMatt Spinler        if mimetype is None:
1266d41643ecSMatt Spinler            print("Can't figure out content-type for %s" % filename)
1267d41643ecSMatt Spinler            mimetype = 'auto'
1268d41643ecSMatt Spinler
1269d41643ecSMatt Spinler        # This call will set several header fields for us,
1270d41643ecSMatt Spinler        # including the charset if the type is text.
1271d41643ecSMatt Spinler        response = static_file(filename, www_base_path, mimetype)
1272d41643ecSMatt Spinler
1273d41643ecSMatt Spinler        # static_file() will only set the encoding if the
1274d41643ecSMatt Spinler        # mimetype was auto, so set it here.
1275d41643ecSMatt Spinler        if encoding is not None:
1276d41643ecSMatt Spinler            response.set_header('Content-Encoding', encoding)
1277d41643ecSMatt Spinler
1278d41643ecSMatt Spinler        return response
1279d41643ecSMatt Spinler
1280d41643ecSMatt Spinler    def find(self, **kw):
1281d41643ecSMatt Spinler        pass
1282d41643ecSMatt Spinler
1283d41643ecSMatt Spinler    def setup(self, **kw):
1284d41643ecSMatt Spinler        pass
1285d41643ecSMatt Spinler
1286d41643ecSMatt Spinler
12872c6fc760SBrad Bishopclass AuthorizationPlugin(object):
12882c6fc760SBrad Bishop    ''' Invokes an optional list of authorization callbacks. '''
12892c6fc760SBrad Bishop
12902c6fc760SBrad Bishop    name = 'authorization'
12912c6fc760SBrad Bishop    api = 2
12922c6fc760SBrad Bishop
12932c6fc760SBrad Bishop    class Compose:
12942c6fc760SBrad Bishop        def __init__(self, validators, callback, session_mgr):
12952c6fc760SBrad Bishop            self.validators = validators
12962c6fc760SBrad Bishop            self.callback = callback
12972c6fc760SBrad Bishop            self.session_mgr = session_mgr
12982c6fc760SBrad Bishop
12992c6fc760SBrad Bishop        def __call__(self, *a, **kw):
13002c6fc760SBrad Bishop            sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
13012c6fc760SBrad Bishop            session = self.session_mgr.get_session(sid)
1302d4c1c55cSBrad Bishop            if request.method != 'OPTIONS':
13032c6fc760SBrad Bishop                for x in self.validators:
13042c6fc760SBrad Bishop                    x(session, *a, **kw)
13052c6fc760SBrad Bishop
13062c6fc760SBrad Bishop            return self.callback(*a, **kw)
13072c6fc760SBrad Bishop
13082c6fc760SBrad Bishop    def apply(self, callback, route):
13092c6fc760SBrad Bishop        undecorated = route.get_undecorated_callback()
13102c6fc760SBrad Bishop        if not isinstance(undecorated, RouteHandler):
13112c6fc760SBrad Bishop            return callback
13122c6fc760SBrad Bishop
13132c6fc760SBrad Bishop        auth_types = getattr(
13142c6fc760SBrad Bishop            undecorated, '_require_auth', None)
13152c6fc760SBrad Bishop        if not auth_types:
13162c6fc760SBrad Bishop            return callback
13172c6fc760SBrad Bishop
13182c6fc760SBrad Bishop        return self.Compose(
13192c6fc760SBrad Bishop            auth_types, callback, undecorated.app.session_handler)
13202c6fc760SBrad Bishop
13212c6fc760SBrad Bishop
1322d0c404a6SBrad Bishopclass CorsPlugin(object):
1323d0c404a6SBrad Bishop    ''' Add CORS headers. '''
1324d0c404a6SBrad Bishop
1325d0c404a6SBrad Bishop    name = 'cors'
1326d0c404a6SBrad Bishop    api = 2
1327d0c404a6SBrad Bishop
1328d0c404a6SBrad Bishop    @staticmethod
1329d0c404a6SBrad Bishop    def process_origin():
1330d0c404a6SBrad Bishop        origin = request.headers.get('Origin')
1331d0c404a6SBrad Bishop        if origin:
1332d0c404a6SBrad Bishop            response.add_header('Access-Control-Allow-Origin', origin)
1333d0c404a6SBrad Bishop            response.add_header(
1334d0c404a6SBrad Bishop                'Access-Control-Allow-Credentials', 'true')
1335d0c404a6SBrad Bishop
1336d0c404a6SBrad Bishop    @staticmethod
1337d0c404a6SBrad Bishop    def process_method_and_headers(verbs):
1338d0c404a6SBrad Bishop        method = request.headers.get('Access-Control-Request-Method')
1339d0c404a6SBrad Bishop        headers = request.headers.get('Access-Control-Request-Headers')
1340d0c404a6SBrad Bishop        if headers:
1341d0c404a6SBrad Bishop            headers = [x.lower() for x in headers.split(',')]
1342d0c404a6SBrad Bishop
1343d0c404a6SBrad Bishop        if method in verbs \
1344d0c404a6SBrad Bishop                and headers == ['content-type']:
1345d0c404a6SBrad Bishop            response.add_header('Access-Control-Allow-Methods', method)
1346d0c404a6SBrad Bishop            response.add_header(
1347d0c404a6SBrad Bishop                'Access-Control-Allow-Headers', 'Content-Type')
134891b46f89SRatan Gupta            response.add_header('X-Frame-Options', 'deny')
134991b46f89SRatan Gupta            response.add_header('X-Content-Type-Options', 'nosniff')
135091b46f89SRatan Gupta            response.add_header('X-XSS-Protection', '1; mode=block')
135191b46f89SRatan Gupta            response.add_header(
135291b46f89SRatan Gupta                'Content-Security-Policy', "default-src 'self'")
135391b46f89SRatan Gupta            response.add_header(
135491b46f89SRatan Gupta                'Strict-Transport-Security',
135591b46f89SRatan Gupta                'max-age=31536000; includeSubDomains; preload')
1356d0c404a6SBrad Bishop
1357d0c404a6SBrad Bishop    def __init__(self, app):
1358d0c404a6SBrad Bishop        app.install_error_callback(self.error_callback)
1359d0c404a6SBrad Bishop
1360d0c404a6SBrad Bishop    def apply(self, callback, route):
1361d0c404a6SBrad Bishop        undecorated = route.get_undecorated_callback()
1362d0c404a6SBrad Bishop        if not isinstance(undecorated, RouteHandler):
1363d0c404a6SBrad Bishop            return callback
1364d0c404a6SBrad Bishop
1365d0c404a6SBrad Bishop        if not getattr(undecorated, '_enable_cors', None):
1366d0c404a6SBrad Bishop            return callback
1367d0c404a6SBrad Bishop
1368d0c404a6SBrad Bishop        def wrap(*a, **kw):
1369d0c404a6SBrad Bishop            self.process_origin()
1370d0c404a6SBrad Bishop            self.process_method_and_headers(undecorated._verbs)
1371d0c404a6SBrad Bishop            return callback(*a, **kw)
1372d0c404a6SBrad Bishop
1373d0c404a6SBrad Bishop        return wrap
1374d0c404a6SBrad Bishop
1375d0c404a6SBrad Bishop    def error_callback(self, **kw):
1376d0c404a6SBrad Bishop        self.process_origin()
1377d0c404a6SBrad Bishop
1378d0c404a6SBrad Bishop
13792c6fc760SBrad Bishopclass JsonApiRequestPlugin(object):
13802c6fc760SBrad Bishop    ''' Ensures request content satisfies the OpenBMC json api format. '''
13812c6fc760SBrad Bishop    name = 'json_api_request'
13822c6fc760SBrad Bishop    api = 2
13832c6fc760SBrad Bishop
13842c6fc760SBrad Bishop    error_str = "Expecting request format { 'data': <value> }, got '%s'"
13852c6fc760SBrad Bishop    type_error_str = "Unsupported Content-Type: '%s'"
13862c6fc760SBrad Bishop    json_type = "application/json"
13872c6fc760SBrad Bishop    request_methods = ['PUT', 'POST', 'PATCH']
13882c6fc760SBrad Bishop
13892c6fc760SBrad Bishop    @staticmethod
13902c6fc760SBrad Bishop    def content_expected():
13912c6fc760SBrad Bishop        return request.method in JsonApiRequestPlugin.request_methods
13922c6fc760SBrad Bishop
13932c6fc760SBrad Bishop    def validate_request(self):
13942c6fc760SBrad Bishop        if request.content_length > 0 and \
13952c6fc760SBrad Bishop                request.content_type != self.json_type:
13962c6fc760SBrad Bishop            abort(415, self.type_error_str % request.content_type)
13972c6fc760SBrad Bishop
13982c6fc760SBrad Bishop        try:
13992c6fc760SBrad Bishop            request.parameter_list = request.json.get('data')
1400249d1327SCamVan Nguyen        except ValueError as e:
14012c6fc760SBrad Bishop            abort(400, str(e))
14022c6fc760SBrad Bishop        except (AttributeError, KeyError, TypeError):
14032c6fc760SBrad Bishop            abort(400, self.error_str % request.json)
14042c6fc760SBrad Bishop
14052c6fc760SBrad Bishop    def apply(self, callback, route):
1406fb6cd483SDeepak Kodihalli        content_type = getattr(
1407fb6cd483SDeepak Kodihalli            route.get_undecorated_callback(), '_content_type', None)
1408fb6cd483SDeepak Kodihalli        if self.json_type != content_type:
1409fb6cd483SDeepak Kodihalli            return callback
1410fb6cd483SDeepak Kodihalli
14112c6fc760SBrad Bishop        verbs = getattr(
14122c6fc760SBrad Bishop            route.get_undecorated_callback(), '_verbs', None)
14132c6fc760SBrad Bishop        if verbs is None:
14142c6fc760SBrad Bishop            return callback
14152c6fc760SBrad Bishop
14162c6fc760SBrad Bishop        if not set(self.request_methods).intersection(verbs):
14172c6fc760SBrad Bishop            return callback
14182c6fc760SBrad Bishop
14192c6fc760SBrad Bishop        def wrap(*a, **kw):
14202c6fc760SBrad Bishop            if self.content_expected():
14212c6fc760SBrad Bishop                self.validate_request()
14222c6fc760SBrad Bishop            return callback(*a, **kw)
14232c6fc760SBrad Bishop
14242c6fc760SBrad Bishop        return wrap
14252c6fc760SBrad Bishop
14262c6fc760SBrad Bishop
14272c6fc760SBrad Bishopclass JsonApiRequestTypePlugin(object):
14282c6fc760SBrad Bishop    ''' Ensures request content type satisfies the OpenBMC json api format. '''
14292c6fc760SBrad Bishop    name = 'json_api_method_request'
14302c6fc760SBrad Bishop    api = 2
14312c6fc760SBrad Bishop
14322c6fc760SBrad Bishop    error_str = "Expecting request format { 'data': %s }, got '%s'"
1433fb6cd483SDeepak Kodihalli    json_type = "application/json"
14342c6fc760SBrad Bishop
14352c6fc760SBrad Bishop    def apply(self, callback, route):
1436fb6cd483SDeepak Kodihalli        content_type = getattr(
1437fb6cd483SDeepak Kodihalli            route.get_undecorated_callback(), '_content_type', None)
1438fb6cd483SDeepak Kodihalli        if self.json_type != content_type:
1439fb6cd483SDeepak Kodihalli            return callback
1440fb6cd483SDeepak Kodihalli
14412c6fc760SBrad Bishop        request_type = getattr(
14422c6fc760SBrad Bishop            route.get_undecorated_callback(), 'request_type', None)
14432c6fc760SBrad Bishop        if request_type is None:
14442c6fc760SBrad Bishop            return callback
14452c6fc760SBrad Bishop
14462c6fc760SBrad Bishop        def validate_request():
14472c6fc760SBrad Bishop            if not isinstance(request.parameter_list, request_type):
14482c6fc760SBrad Bishop                abort(400, self.error_str % (str(request_type), request.json))
14492c6fc760SBrad Bishop
14502c6fc760SBrad Bishop        def wrap(*a, **kw):
14512c6fc760SBrad Bishop            if JsonApiRequestPlugin.content_expected():
14522c6fc760SBrad Bishop                validate_request()
14532c6fc760SBrad Bishop            return callback(*a, **kw)
14542c6fc760SBrad Bishop
14552c6fc760SBrad Bishop        return wrap
14562c6fc760SBrad Bishop
14572c6fc760SBrad Bishop
1458080a48edSBrad Bishopclass JsonErrorsPlugin(JSONPlugin):
1459080a48edSBrad Bishop    ''' Extend the Bottle JSONPlugin such that it also encodes error
1460080a48edSBrad Bishop        responses. '''
1461080a48edSBrad Bishop
1462080a48edSBrad Bishop    def __init__(self, app, **kw):
1463080a48edSBrad Bishop        super(JsonErrorsPlugin, self).__init__(**kw)
1464080a48edSBrad Bishop        self.json_opts = {
1465249d1327SCamVan Nguyen            x: y for x, y in kw.items()
1466080a48edSBrad Bishop            if x in ['indent', 'sort_keys']}
1467080a48edSBrad Bishop        app.install_error_callback(self.error_callback)
1468080a48edSBrad Bishop
1469080a48edSBrad Bishop    def error_callback(self, response_object, response_body, **kw):
1470080a48edSBrad Bishop        response_body['body'] = json.dumps(response_object, **self.json_opts)
1471080a48edSBrad Bishop        response.content_type = 'application/json'
1472080a48edSBrad Bishop
1473080a48edSBrad Bishop
14742c6fc760SBrad Bishopclass JsonApiResponsePlugin(object):
1475080a48edSBrad Bishop    ''' Emits responses in the OpenBMC json api format. '''
14762c6fc760SBrad Bishop    name = 'json_api_response'
14772c6fc760SBrad Bishop    api = 2
14782c6fc760SBrad Bishop
1479d4c1c55cSBrad Bishop    @staticmethod
1480d4c1c55cSBrad Bishop    def has_body():
1481d4c1c55cSBrad Bishop        return request.method not in ['OPTIONS']
1482d4c1c55cSBrad Bishop
1483080a48edSBrad Bishop    def __init__(self, app):
1484080a48edSBrad Bishop        app.install_error_callback(self.error_callback)
1485080a48edSBrad Bishop
14866691e7caSMatt Spinler    @staticmethod
14876691e7caSMatt Spinler    def dbus_boolean_to_bool(data):
14886691e7caSMatt Spinler        ''' Convert all dbus.Booleans to true/false instead of 1/0 as
14896691e7caSMatt Spinler            the JSON encoder thinks they're ints.  Note that unlike
14906691e7caSMatt Spinler            dicts and lists, tuples (from a dbus.Struct) are immutable
14916691e7caSMatt Spinler            so they need special handling. '''
14926691e7caSMatt Spinler
14936691e7caSMatt Spinler        def walkdict(data):
14946691e7caSMatt Spinler            for key, value in data.items():
14956691e7caSMatt Spinler                if isinstance(value, dbus.Boolean):
14966691e7caSMatt Spinler                    data[key] = bool(value)
14976691e7caSMatt Spinler                elif isinstance(value, tuple):
14986691e7caSMatt Spinler                    data[key] = walktuple(value)
14996691e7caSMatt Spinler                else:
15006691e7caSMatt Spinler                    JsonApiResponsePlugin.dbus_boolean_to_bool(value)
15016691e7caSMatt Spinler
15026691e7caSMatt Spinler        def walklist(data):
15036691e7caSMatt Spinler            for i in range(len(data)):
15046691e7caSMatt Spinler                if isinstance(data[i], dbus.Boolean):
15056691e7caSMatt Spinler                    data[i] = bool(data[i])
15066691e7caSMatt Spinler                elif isinstance(data[i], tuple):
15076691e7caSMatt Spinler                    data[i] = walktuple(data[i])
15086691e7caSMatt Spinler                else:
15096691e7caSMatt Spinler                    JsonApiResponsePlugin.dbus_boolean_to_bool(data[i])
15106691e7caSMatt Spinler
15116691e7caSMatt Spinler        def walktuple(data):
15126691e7caSMatt Spinler            new = []
15136691e7caSMatt Spinler            for item in data:
15146691e7caSMatt Spinler                if isinstance(item, dbus.Boolean):
15156691e7caSMatt Spinler                    item = bool(item)
15166691e7caSMatt Spinler                else:
15176691e7caSMatt Spinler                    JsonApiResponsePlugin.dbus_boolean_to_bool(item)
15186691e7caSMatt Spinler                new.append(item)
15196691e7caSMatt Spinler            return tuple(new)
15206691e7caSMatt Spinler
15216691e7caSMatt Spinler        if isinstance(data, dict):
15226691e7caSMatt Spinler            walkdict(data)
15236691e7caSMatt Spinler        elif isinstance(data, list):
15246691e7caSMatt Spinler            walklist(data)
15256691e7caSMatt Spinler
15262c6fc760SBrad Bishop    def apply(self, callback, route):
1527944cd04fSBrad Bishop        skip = getattr(
1528944cd04fSBrad Bishop            route.get_undecorated_callback(), 'suppress_json_resp', None)
1529944cd04fSBrad Bishop        if skip:
15301444fd8cSJayanth Othayoth            return callback
15311444fd8cSJayanth Othayoth
15322c6fc760SBrad Bishop        def wrap(*a, **kw):
1533d4c1c55cSBrad Bishop            data = callback(*a, **kw)
15346691e7caSMatt Spinler            JsonApiResponsePlugin.dbus_boolean_to_bool(data)
1535d4c1c55cSBrad Bishop            if self.has_body():
1536d4c1c55cSBrad Bishop                resp = {'data': data}
15372c6fc760SBrad Bishop                resp['status'] = 'ok'
15382c6fc760SBrad Bishop                resp['message'] = response.status_line
15392c6fc760SBrad Bishop                return resp
15402c6fc760SBrad Bishop        return wrap
15412c6fc760SBrad Bishop
1542080a48edSBrad Bishop    def error_callback(self, error, response_object, **kw):
15432c6fc760SBrad Bishop        response_object['message'] = error.status_line
15449c2531eeSBrad Bishop        response_object['status'] = 'error'
1545080a48edSBrad Bishop        response_object.setdefault('data', {})['description'] = str(error.body)
15462c6fc760SBrad Bishop        if error.status_code == 500:
15472c6fc760SBrad Bishop            response_object['data']['exception'] = repr(error.exception)
15482c6fc760SBrad Bishop            response_object['data']['traceback'] = error.traceback.splitlines()
15492c6fc760SBrad Bishop
15502c6fc760SBrad Bishop
1551080a48edSBrad Bishopclass JsonpPlugin(object):
15522c6fc760SBrad Bishop    ''' Json javascript wrapper. '''
15532c6fc760SBrad Bishop    name = 'jsonp'
15542c6fc760SBrad Bishop    api = 2
15552c6fc760SBrad Bishop
1556080a48edSBrad Bishop    def __init__(self, app, **kw):
1557080a48edSBrad Bishop        app.install_error_callback(self.error_callback)
15582c6fc760SBrad Bishop
15592c6fc760SBrad Bishop    @staticmethod
15602c6fc760SBrad Bishop    def to_jsonp(json):
15612c6fc760SBrad Bishop        jwrapper = request.query.callback or None
15622c6fc760SBrad Bishop        if(jwrapper):
15632c6fc760SBrad Bishop            response.set_header('Content-Type', 'application/javascript')
15642c6fc760SBrad Bishop            json = jwrapper + '(' + json + ');'
15652c6fc760SBrad Bishop        return json
15662c6fc760SBrad Bishop
15672c6fc760SBrad Bishop    def apply(self, callback, route):
15682c6fc760SBrad Bishop        def wrap(*a, **kw):
15692c6fc760SBrad Bishop            return self.to_jsonp(callback(*a, **kw))
15702c6fc760SBrad Bishop        return wrap
15712c6fc760SBrad Bishop
1572080a48edSBrad Bishop    def error_callback(self, response_body, **kw):
1573080a48edSBrad Bishop        response_body['body'] = self.to_jsonp(response_body['body'])
15742c6fc760SBrad Bishop
15752c6fc760SBrad Bishop
1576461367a5SDeepak Kodihalliclass ContentCheckerPlugin(object):
1577461367a5SDeepak Kodihalli    ''' Ensures that a route is associated with the expected content-type
1578461367a5SDeepak Kodihalli        header. '''
1579461367a5SDeepak Kodihalli    name = 'content_checker'
1580461367a5SDeepak Kodihalli    api = 2
1581461367a5SDeepak Kodihalli
1582461367a5SDeepak Kodihalli    class Checker:
1583461367a5SDeepak Kodihalli        def __init__(self, type, callback):
1584461367a5SDeepak Kodihalli            self.expected_type = type
1585461367a5SDeepak Kodihalli            self.callback = callback
1586461367a5SDeepak Kodihalli            self.error_str = "Expecting content type '%s', got '%s'"
1587461367a5SDeepak Kodihalli
1588461367a5SDeepak Kodihalli        def __call__(self, *a, **kw):
1589db1a21e3SDeepak Kodihalli            if request.method in ['PUT', 'POST', 'PATCH'] and \
1590db1a21e3SDeepak Kodihalli                    self.expected_type and \
1591461367a5SDeepak Kodihalli                    self.expected_type != request.content_type:
1592461367a5SDeepak Kodihalli                abort(415, self.error_str % (self.expected_type,
1593461367a5SDeepak Kodihalli                      request.content_type))
1594461367a5SDeepak Kodihalli
1595461367a5SDeepak Kodihalli            return self.callback(*a, **kw)
1596461367a5SDeepak Kodihalli
1597461367a5SDeepak Kodihalli    def apply(self, callback, route):
1598461367a5SDeepak Kodihalli        content_type = getattr(
1599461367a5SDeepak Kodihalli            route.get_undecorated_callback(), '_content_type', None)
1600461367a5SDeepak Kodihalli
1601461367a5SDeepak Kodihalli        return self.Checker(content_type, callback)
1602461367a5SDeepak Kodihalli
1603461367a5SDeepak Kodihalli
16046e1ca530SDeepak Kodihalliclass LoggingPlugin(object):
16056e1ca530SDeepak Kodihalli    ''' Wraps a request in order to emit a log after the request is handled. '''
16066e1ca530SDeepak Kodihalli    name = 'loggingp'
16076e1ca530SDeepak Kodihalli    api = 2
16086e1ca530SDeepak Kodihalli
16096e1ca530SDeepak Kodihalli    class Logger:
16106e1ca530SDeepak Kodihalli        def __init__(self, suppress_json_logging, callback, app):
16116e1ca530SDeepak Kodihalli            self.suppress_json_logging = suppress_json_logging
16126e1ca530SDeepak Kodihalli            self.callback = callback
16136e1ca530SDeepak Kodihalli            self.app = app
161495803684SDeepak Kodihalli            self.logging_enabled = None
161595803684SDeepak Kodihalli            self.bus = dbus.SystemBus()
161695803684SDeepak Kodihalli            self.dbus_path = '/xyz/openbmc_project/logging/rest_api_logs'
16174b412ac9SDeepak Kodihalli            self.no_json = [
16184b412ac9SDeepak Kodihalli                '/xyz/openbmc_project/user/ldap/action/CreateConfig'
16194b412ac9SDeepak Kodihalli            ]
162095803684SDeepak Kodihalli            self.bus.add_signal_receiver(
162195803684SDeepak Kodihalli                self.properties_changed_handler,
162295803684SDeepak Kodihalli                dbus_interface=dbus.PROPERTIES_IFACE,
162395803684SDeepak Kodihalli                signal_name='PropertiesChanged',
162495803684SDeepak Kodihalli                path=self.dbus_path)
162595803684SDeepak Kodihalli            Greenlet.spawn(self.dbus_loop)
16266e1ca530SDeepak Kodihalli
16276e1ca530SDeepak Kodihalli        def __call__(self, *a, **kw):
16286e1ca530SDeepak Kodihalli            resp = self.callback(*a, **kw)
162995803684SDeepak Kodihalli            if not self.enabled():
16304aa10001SDeepak Kodihalli                return resp
16316e1ca530SDeepak Kodihalli            if request.method == 'GET':
16324aa10001SDeepak Kodihalli                return resp
16336e1ca530SDeepak Kodihalli            json = request.json
16346e1ca530SDeepak Kodihalli            if self.suppress_json_logging:
16356e1ca530SDeepak Kodihalli                json = None
16364b412ac9SDeepak Kodihalli            elif any(substring in request.url for substring in self.no_json):
16374b412ac9SDeepak Kodihalli                json = None
16386e1ca530SDeepak Kodihalli            session = self.app.session_handler.get_session_from_cookie()
16396e1ca530SDeepak Kodihalli            user = None
16406e1ca530SDeepak Kodihalli            if "/login" in request.url:
16416e1ca530SDeepak Kodihalli                user = request.parameter_list[0]
16426e1ca530SDeepak Kodihalli            elif session is not None:
16436e1ca530SDeepak Kodihalli                user = session['user']
16446e1ca530SDeepak Kodihalli            print("{remote} user:{user} {method} {url} json:{json} {status}" \
16456e1ca530SDeepak Kodihalli                .format(
16466e1ca530SDeepak Kodihalli                    user=user,
16476e1ca530SDeepak Kodihalli                    remote=request.remote_addr,
16486e1ca530SDeepak Kodihalli                    method=request.method,
16496e1ca530SDeepak Kodihalli                    url=request.url,
16506e1ca530SDeepak Kodihalli                    json=json,
16516e1ca530SDeepak Kodihalli                    status=response.status))
16524aa10001SDeepak Kodihalli            return resp
16536e1ca530SDeepak Kodihalli
165495803684SDeepak Kodihalli        def enabled(self):
165595803684SDeepak Kodihalli            if self.logging_enabled is None:
165695803684SDeepak Kodihalli                try:
165795803684SDeepak Kodihalli                    obj = self.bus.get_object(
165895803684SDeepak Kodihalli                              'xyz.openbmc_project.Settings',
165995803684SDeepak Kodihalli                              self.dbus_path)
166095803684SDeepak Kodihalli                    iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
166195803684SDeepak Kodihalli                    logging_enabled = iface.Get(
166295803684SDeepak Kodihalli                                          'xyz.openbmc_project.Object.Enable',
166395803684SDeepak Kodihalli                                          'Enabled')
166495803684SDeepak Kodihalli                    self.logging_enabled = logging_enabled
166595803684SDeepak Kodihalli                except dbus.exceptions.DBusException:
166695803684SDeepak Kodihalli                    self.logging_enabled = False
166795803684SDeepak Kodihalli            return self.logging_enabled
166895803684SDeepak Kodihalli
166995803684SDeepak Kodihalli        def dbus_loop(self):
167095803684SDeepak Kodihalli            loop = gobject.MainLoop()
167195803684SDeepak Kodihalli            gcontext = loop.get_context()
167295803684SDeepak Kodihalli            while loop is not None:
167395803684SDeepak Kodihalli                try:
167495803684SDeepak Kodihalli                    if gcontext.pending():
167595803684SDeepak Kodihalli                        gcontext.iteration()
167695803684SDeepak Kodihalli                    else:
167795803684SDeepak Kodihalli                        gevent.sleep(5)
167895803684SDeepak Kodihalli                except Exception as e:
167995803684SDeepak Kodihalli                    break
168095803684SDeepak Kodihalli
168195803684SDeepak Kodihalli        def properties_changed_handler(self, interface, new, old, **kw):
168295803684SDeepak Kodihalli            self.logging_enabled = new.values()[0]
168395803684SDeepak Kodihalli
16846e1ca530SDeepak Kodihalli    def apply(self, callback, route):
16856e1ca530SDeepak Kodihalli        cb = route.get_undecorated_callback()
16866e1ca530SDeepak Kodihalli        skip = getattr(
16876e1ca530SDeepak Kodihalli            cb, 'suppress_logging', None)
16886e1ca530SDeepak Kodihalli        if skip:
16896e1ca530SDeepak Kodihalli            return callback
16906e1ca530SDeepak Kodihalli
16916e1ca530SDeepak Kodihalli        suppress_json_logging = getattr(
16926e1ca530SDeepak Kodihalli            cb, 'suppress_json_logging', None)
16936e1ca530SDeepak Kodihalli        return self.Logger(suppress_json_logging, callback, cb.app)
16946e1ca530SDeepak Kodihalli
16956e1ca530SDeepak Kodihalli
16962c6fc760SBrad Bishopclass App(Bottle):
16970fe213fbSDeepak Kodihalli    def __init__(self, **kw):
16982c6fc760SBrad Bishop        super(App, self).__init__(autojson=False)
1699b209dd16SDeepak Kodihalli
1700b209dd16SDeepak Kodihalli        self.have_wsock = kw.get('have_wsock', False)
1701d08a4569SAlexander Filippov        self.with_bmc_check = '--with-bmc-check' in sys.argv
1702b209dd16SDeepak Kodihalli
17032c6fc760SBrad Bishop        self.bus = dbus.SystemBus()
17042c6fc760SBrad Bishop        self.mapper = obmc.mapper.Mapper(self.bus)
1705080a48edSBrad Bishop        self.error_callbacks = []
17062c6fc760SBrad Bishop
17072c6fc760SBrad Bishop        self.install_hooks()
17082c6fc760SBrad Bishop        self.install_plugins()
17092c6fc760SBrad Bishop        self.create_handlers()
17102c6fc760SBrad Bishop        self.install_handlers()
17112c6fc760SBrad Bishop
17122c6fc760SBrad Bishop    def install_plugins(self):
17132c6fc760SBrad Bishop        # install json api plugins
17142c6fc760SBrad Bishop        json_kw = {'indent': 2, 'sort_keys': True}
17152c6fc760SBrad Bishop        self.install(AuthorizationPlugin())
1716d0c404a6SBrad Bishop        self.install(CorsPlugin(self))
1717461367a5SDeepak Kodihalli        self.install(ContentCheckerPlugin())
1718080a48edSBrad Bishop        self.install(JsonpPlugin(self, **json_kw))
1719080a48edSBrad Bishop        self.install(JsonErrorsPlugin(self, **json_kw))
1720080a48edSBrad Bishop        self.install(JsonApiResponsePlugin(self))
17212c6fc760SBrad Bishop        self.install(JsonApiRequestPlugin())
17222c6fc760SBrad Bishop        self.install(JsonApiRequestTypePlugin())
17236e1ca530SDeepak Kodihalli        self.install(LoggingPlugin())
17242c6fc760SBrad Bishop
17252c6fc760SBrad Bishop    def install_hooks(self):
1726080a48edSBrad Bishop        self.error_handler_type = type(self.default_error_handler)
1727080a48edSBrad Bishop        self.original_error_handler = self.default_error_handler
1728080a48edSBrad Bishop        self.default_error_handler = self.error_handler_type(
1729080a48edSBrad Bishop            self.custom_error_handler, self, Bottle)
1730080a48edSBrad Bishop
17312c6fc760SBrad Bishop        self.real_router_match = self.router.match
17322c6fc760SBrad Bishop        self.router.match = self.custom_router_match
17332c6fc760SBrad Bishop        self.add_hook('before_request', self.strip_extra_slashes)
17342c6fc760SBrad Bishop
17352c6fc760SBrad Bishop    def create_handlers(self):
17362c6fc760SBrad Bishop        # create route handlers
17372c6fc760SBrad Bishop        self.session_handler = SessionHandler(self, self.bus)
1738d41643ecSMatt Spinler        self.web_handler = WebHandler(self, self.bus)
17392c6fc760SBrad Bishop        self.directory_handler = DirectoryHandler(self, self.bus)
17402c6fc760SBrad Bishop        self.list_names_handler = ListNamesHandler(self, self.bus)
17412c6fc760SBrad Bishop        self.list_handler = ListHandler(self, self.bus)
17422c6fc760SBrad Bishop        self.method_handler = MethodHandler(self, self.bus)
17432c6fc760SBrad Bishop        self.property_handler = PropertyHandler(self, self.bus)
17442c6fc760SBrad Bishop        self.schema_handler = SchemaHandler(self, self.bus)
17457ec0a4f4SDeepak Kodihalli        self.image_upload_post_handler = ImagePostHandler(self, self.bus)
17467ec0a4f4SDeepak Kodihalli        self.image_upload_put_handler = ImagePutHandler(self, self.bus)
17479bc94997SJayanth Othayoth        self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
1748dee2ef57SDhruvaraj Subhashchandran        self.certificate_put_handler = CertificatePutHandler(self, self.bus)
1749b209dd16SDeepak Kodihalli        if self.have_wsock:
1750b209dd16SDeepak Kodihalli            self.event_handler = EventHandler(self, self.bus)
17515c518f63SDeepak Kodihalli            self.host_console_handler = HostConsoleHandler(self, self.bus)
17522c6fc760SBrad Bishop        self.instance_handler = InstanceHandler(self, self.bus)
17532c6fc760SBrad Bishop
17542c6fc760SBrad Bishop    def install_handlers(self):
17552c6fc760SBrad Bishop        self.session_handler.install()
1756d41643ecSMatt Spinler        self.web_handler.install()
17572c6fc760SBrad Bishop        self.directory_handler.install()
17582c6fc760SBrad Bishop        self.list_names_handler.install()
17592c6fc760SBrad Bishop        self.list_handler.install()
17602c6fc760SBrad Bishop        self.method_handler.install()
17612c6fc760SBrad Bishop        self.property_handler.install()
17622c6fc760SBrad Bishop        self.schema_handler.install()
17637ec0a4f4SDeepak Kodihalli        self.image_upload_post_handler.install()
17647ec0a4f4SDeepak Kodihalli        self.image_upload_put_handler.install()
17659bc94997SJayanth Othayoth        self.download_dump_get_handler.install()
1766dee2ef57SDhruvaraj Subhashchandran        self.certificate_put_handler.install()
1767b209dd16SDeepak Kodihalli        if self.have_wsock:
1768b209dd16SDeepak Kodihalli            self.event_handler.install()
17695c518f63SDeepak Kodihalli            self.host_console_handler.install()
17702c6fc760SBrad Bishop        # this has to come last, since it matches everything
17712c6fc760SBrad Bishop        self.instance_handler.install()
17722c6fc760SBrad Bishop
1773080a48edSBrad Bishop    def install_error_callback(self, callback):
1774080a48edSBrad Bishop        self.error_callbacks.insert(0, callback)
1775080a48edSBrad Bishop
17762c6fc760SBrad Bishop    def custom_router_match(self, environ):
17772c6fc760SBrad Bishop        ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
17782c6fc760SBrad Bishop            needed doesn't work for us since the instance rules match
17792c6fc760SBrad Bishop            everything. This monkey-patch lets the route handler figure
17802c6fc760SBrad Bishop            out which response is needed.  This could be accomplished
17812c6fc760SBrad Bishop            with a hook but that would require calling the router match
17822c6fc760SBrad Bishop            function twice.
17832c6fc760SBrad Bishop        '''
17842c6fc760SBrad Bishop        route, args = self.real_router_match(environ)
17852c6fc760SBrad Bishop        if isinstance(route.callback, RouteHandler):
17862c6fc760SBrad Bishop            route.callback._setup(**args)
17872c6fc760SBrad Bishop
17882c6fc760SBrad Bishop        return route, args
17892c6fc760SBrad Bishop
1790080a48edSBrad Bishop    def custom_error_handler(self, res, error):
1791f01d0ba3SGunnar Mills        ''' Allow plugins to modify error responses too via this custom
1792080a48edSBrad Bishop            error handler. '''
1793080a48edSBrad Bishop
1794080a48edSBrad Bishop        response_object = {}
1795080a48edSBrad Bishop        response_body = {}
1796080a48edSBrad Bishop        for x in self.error_callbacks:
1797080a48edSBrad Bishop            x(error=error,
1798080a48edSBrad Bishop                response_object=response_object,
1799080a48edSBrad Bishop                response_body=response_body)
1800080a48edSBrad Bishop
1801080a48edSBrad Bishop        return response_body.get('body', "")
1802080a48edSBrad Bishop
18032c6fc760SBrad Bishop    @staticmethod
18042c6fc760SBrad Bishop    def strip_extra_slashes():
18052c6fc760SBrad Bishop        path = request.environ['PATH_INFO']
18062c6fc760SBrad Bishop        trailing = ("", "/")[path[-1] == '/']
1807249d1327SCamVan Nguyen        parts = list(filter(bool, path.split('/')))
18082c6fc760SBrad Bishop        request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
1809