xref: /openbmc/phosphor-rest-server/module/obmc/wsgi/apps/rest_dbus.py (revision 8cc1a034b264d8771648840f3bc57f07ccad72fa)
1# Contributors Listed Below - COPYRIGHT 2016
2# [+] International Business Machines Corp.
3#
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied. See the License for the specific language governing
15# permissions and limitations under the License.
16
17import os
18import dbus
19import dbus.exceptions
20import json
21from xml.etree import ElementTree
22from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
23from bottle import static_file
24import obmc.utils.misc
25from obmc.dbuslib.introspection import IntrospectionNodeParser
26import obmc.mapper
27import spwd
28import grp
29import crypt
30import tempfile
31import re
32have_wsock = True
33try:
34    from geventwebsocket import WebSocketError
35except ImportError:
36    have_wsock = False
37if have_wsock:
38    from dbus.mainloop.glib import DBusGMainLoop
39    DBusGMainLoop(set_as_default=True)
40    import gobject
41    import gevent
42
43DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
44DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
45DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
46DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
47DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
48DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
49
50_4034_msg = "The specified %s cannot be %s: '%s'"
51
52
53def valid_user(session, *a, **kw):
54    ''' Authorization plugin callback that checks
55    that the user is logged in. '''
56    if session is None:
57        abort(401, 'Login required')
58
59
60def get_type_signature_by_introspection(bus, service, object_path,
61                                        property_name):
62    obj = bus.get_object(service, object_path)
63    iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
64    xml_string = iface.Introspect()
65    for child in ElementTree.fromstring(xml_string):
66        # Iterate over each interfaces's properties to find
67        # matching property_name, and return its signature string
68        if child.tag == 'interface':
69            for i in child.iter():
70                if ('name' in i.attrib) and \
71                   (i.attrib['name'] == property_name):
72                    type_signature = i.attrib['type']
73                    return type_signature
74
75
76def get_method_signature(bus, service, object_path, interface, method):
77    obj = bus.get_object(service, object_path)
78    iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
79    xml_string = iface.Introspect()
80    arglist = []
81
82    root = ElementTree.fromstring(xml_string)
83    for dbus_intf in root.findall('interface'):
84        if (dbus_intf.get('name') == interface):
85            for dbus_method in dbus_intf.findall('method'):
86                if(dbus_method.get('name') == method):
87                    for arg in dbus_method.findall('arg'):
88                        arglist.append(arg.get('type'))
89                    return arglist
90
91
92def split_struct_signature(signature):
93    struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
94    struct_matches = re.findall(struct_regex, signature)
95    return struct_matches
96
97
98def convert_type(signature, value):
99    # Basic Types
100    converted_value = None
101    converted_container = None
102    basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
103                   'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
104                   't': dbus.UInt64, 'd': float, 's': str}
105    array_matches = re.match(r'a\((\S+)\)', signature)
106    struct_matches = re.match(r'\((\S+)\)', signature)
107    dictionary_matches = re.match(r'a{(\S+)}', signature)
108    if signature in basic_types:
109        converted_value = basic_types[signature](value)
110        return converted_value
111    # Array
112    if array_matches:
113        element_type = array_matches.group(1)
114        converted_container = list()
115        # Test if value is a list
116        # to avoid iterating over each character in a string.
117        # Iterate over each item and convert type
118        if isinstance(value, list):
119            for i in value:
120                converted_element = convert_type(element_type, i)
121                converted_container.append(converted_element)
122        # Convert non-sequence to expected type, and append to list
123        else:
124            converted_element = convert_type(element_type, value)
125            converted_container.append(converted_element)
126        return converted_container
127    # Struct
128    if struct_matches:
129        element_types = struct_matches.group(1)
130        split_element_types = split_struct_signature(element_types)
131        converted_container = list()
132        # Test if value is a list
133        if isinstance(value, list):
134            for index, val in enumerate(value):
135                converted_element = convert_type(split_element_types[index],
136                                                 value[index])
137                converted_container.append(converted_element)
138        else:
139            converted_element = convert_type(element_types, value)
140            converted_container.append(converted_element)
141        return tuple(converted_container)
142    # Dictionary
143    if dictionary_matches:
144        element_types = dictionary_matches.group(1)
145        split_element_types = split_struct_signature(element_types)
146        converted_container = dict()
147        # Convert each element of dict
148        for key, val in value.iteritems():
149            converted_key = convert_type(split_element_types[0], key)
150            converted_val = convert_type(split_element_types[1], val)
151            converted_container[converted_key] = converted_val
152        return converted_container
153
154
155class UserInGroup:
156    ''' Authorization plugin callback that checks that the user is logged in
157    and a member of a group. '''
158    def __init__(self, group):
159        self.group = group
160
161    def __call__(self, session, *a, **kw):
162        valid_user(session, *a, **kw)
163        res = False
164
165        try:
166            res = session['user'] in grp.getgrnam(self.group)[3]
167        except KeyError:
168            pass
169
170        if not res:
171            abort(403, 'Insufficient access')
172
173
174class RouteHandler(object):
175    _require_auth = obmc.utils.misc.makelist(valid_user)
176    _enable_cors = True
177
178    def __init__(self, app, bus, verbs, rules, content_type=''):
179        self.app = app
180        self.bus = bus
181        self.mapper = obmc.mapper.Mapper(bus)
182        self._verbs = obmc.utils.misc.makelist(verbs)
183        self._rules = rules
184        self._content_type = content_type
185
186        if 'GET' in self._verbs:
187            self._verbs = list(set(self._verbs + ['HEAD']))
188        if 'OPTIONS' not in self._verbs:
189            self._verbs.append('OPTIONS')
190
191    def _setup(self, **kw):
192        request.route_data = {}
193
194        if request.method in self._verbs:
195            if request.method != 'OPTIONS':
196                return self.setup(**kw)
197
198            # Javascript implementations will not send credentials
199            # with an OPTIONS request.  Don't help malicious clients
200            # by checking the path here and returning a 404 if the
201            # path doesn't exist.
202            return None
203
204        # Return 405
205        raise HTTPError(
206            405, "Method not allowed.", Allow=','.join(self._verbs))
207
208    def __call__(self, **kw):
209        return getattr(self, 'do_' + request.method.lower())(**kw)
210
211    def do_head(self, **kw):
212        return self.do_get(**kw)
213
214    def do_options(self, **kw):
215        for v in self._verbs:
216            response.set_header(
217                'Allow',
218                ','.join(self._verbs))
219        return None
220
221    def install(self):
222        self.app.route(
223            self._rules, callback=self,
224            method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
225
226    @staticmethod
227    def try_mapper_call(f, callback=None, **kw):
228        try:
229            return f(**kw)
230        except dbus.exceptions.DBusException, e:
231            if e.get_dbus_name() == \
232                    'org.freedesktop.DBus.Error.ObjectPathInUse':
233                abort(503, str(e))
234            if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
235                raise
236            if callback is None:
237                def callback(e, **kw):
238                    abort(404, str(e))
239
240            callback(e, **kw)
241
242    @staticmethod
243    def try_properties_interface(f, *a):
244        try:
245            return f(*a)
246        except dbus.exceptions.DBusException, e:
247            if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
248                # interface doesn't have any properties
249                return None
250            if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
251                # interface doesn't have any properties
252                return None
253            if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
254                # properties interface not implemented at all
255                return None
256            raise
257
258
259class DirectoryHandler(RouteHandler):
260    verbs = 'GET'
261    rules = '<path:path>/'
262
263    def __init__(self, app, bus):
264        super(DirectoryHandler, self).__init__(
265            app, bus, self.verbs, self.rules)
266
267    def find(self, path='/'):
268        return self.try_mapper_call(
269            self.mapper.get_subtree_paths, path=path, depth=1)
270
271    def setup(self, path='/'):
272        request.route_data['map'] = self.find(path)
273
274    def do_get(self, path='/'):
275        return request.route_data['map']
276
277
278class ListNamesHandler(RouteHandler):
279    verbs = 'GET'
280    rules = ['/list', '<path:path>/list']
281
282    def __init__(self, app, bus):
283        super(ListNamesHandler, self).__init__(
284            app, bus, self.verbs, self.rules)
285
286    def find(self, path='/'):
287        return self.try_mapper_call(
288            self.mapper.get_subtree, path=path).keys()
289
290    def setup(self, path='/'):
291        request.route_data['map'] = self.find(path)
292
293    def do_get(self, path='/'):
294        return request.route_data['map']
295
296
297class ListHandler(RouteHandler):
298    verbs = 'GET'
299    rules = ['/enumerate', '<path:path>/enumerate']
300
301    def __init__(self, app, bus):
302        super(ListHandler, self).__init__(
303            app, bus, self.verbs, self.rules)
304
305    def find(self, path='/'):
306        return self.try_mapper_call(
307            self.mapper.get_subtree, path=path)
308
309    def setup(self, path='/'):
310        request.route_data['map'] = self.find(path)
311
312    def do_get(self, path='/'):
313        return {x: y for x, y in self.mapper.enumerate_subtree(
314                path,
315                mapper_data=request.route_data['map']).dataitems()}
316
317
318class MethodHandler(RouteHandler):
319    verbs = 'POST'
320    rules = '<path:path>/action/<method>'
321    request_type = list
322    content_type = 'application/json'
323
324    def __init__(self, app, bus):
325        super(MethodHandler, self).__init__(
326            app, bus, self.verbs, self.rules, self.content_type)
327        self.service = ''
328        self.interface = ''
329
330    def find(self, path, method):
331        method_list = []
332        busses = self.try_mapper_call(
333            self.mapper.get_object, path=path)
334        for items in busses.iteritems():
335            m = self.find_method_on_bus(path, method, *items)
336            if m:
337                method_list.append(m)
338        if method_list:
339            return method_list
340
341        abort(404, _4034_msg % ('method', 'found', method))
342
343    def setup(self, path, method):
344        request.route_data['map'] = self.find(path, method)
345
346    def do_post(self, path, method):
347        try:
348            args = []
349            if request.parameter_list:
350                args = request.parameter_list
351            # To see if the return type is capable of being merged
352            if len(request.route_data['map']) > 1:
353                results = None
354                for item in request.route_data['map']:
355                    tmp = item(*args)
356                    if not results:
357                        if tmp is not None:
358                            results = type(tmp)()
359                    if isinstance(results, dict):
360                        results = results.update(tmp)
361                    elif isinstance(results, list):
362                        results = results + tmp
363                    elif isinstance(results, type(None)):
364                        results = None
365                    else:
366                        abort(501, 'Don\'t know how to merge method call '
367                                   'results of {}'.format(type(tmp)))
368                return results
369            # There is only one method
370            return request.route_data['map'][0](*args)
371
372        except dbus.exceptions.DBusException, e:
373            paramlist = []
374            if e.get_dbus_name() == DBUS_INVALID_ARGS:
375
376                signature_list = get_method_signature(self.bus, self.service,
377                                                      path, self.interface,
378                                                      method)
379                if not signature_list:
380                    abort(400, "Failed to get method signature: %s" % str(e))
381                if len(signature_list) != len(request.parameter_list):
382                    abort(400, "Invalid number of args")
383                converted_value = None
384                try:
385                    for index, expected_type in enumerate(signature_list):
386                        value = request.parameter_list[index]
387                        converted_value = convert_type(expected_type, value)
388                        paramlist.append(converted_value)
389                    request.parameter_list = paramlist
390                    self.do_post(path, method)
391                    return
392                except Exception as ex:
393                    abort(400, "Failed to convert the types")
394                abort(400, str(e))
395
396            if e.get_dbus_name() == DBUS_TYPE_ERROR:
397                abort(400, str(e))
398            raise
399
400    @staticmethod
401    def find_method_in_interface(method, obj, interface, methods):
402        if methods is None:
403            return None
404
405        method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
406        if method is not None:
407            iface = dbus.Interface(obj, interface)
408            return iface.get_dbus_method(method)
409
410    def find_method_on_bus(self, path, method, bus, interfaces):
411        obj = self.bus.get_object(bus, path, introspect=False)
412        iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
413        data = iface.Introspect()
414        parser = IntrospectionNodeParser(
415            ElementTree.fromstring(data),
416            intf_match=obmc.utils.misc.ListMatch(interfaces))
417        for x, y in parser.get_interfaces().iteritems():
418            m = self.find_method_in_interface(
419                method, obj, x, y.get('method'))
420            if m:
421                self.service = bus
422                self.interface = x
423                return m
424
425
426class PropertyHandler(RouteHandler):
427    verbs = ['PUT', 'GET']
428    rules = '<path:path>/attr/<prop>'
429    content_type = 'application/json'
430
431    def __init__(self, app, bus):
432        super(PropertyHandler, self).__init__(
433            app, bus, self.verbs, self.rules, self.content_type)
434
435    def find(self, path, prop):
436        self.app.instance_handler.setup(path)
437        obj = self.app.instance_handler.do_get(path)
438        real_name = obmc.utils.misc.find_case_insensitive(
439            prop, obj.keys())
440
441        if not real_name:
442            if request.method == 'PUT':
443                abort(403, _4034_msg % ('property', 'created', prop))
444            else:
445                abort(404, _4034_msg % ('property', 'found', prop))
446        return real_name, {path: obj}
447
448    def setup(self, path, prop):
449        name, obj = self.find(path, prop)
450        request.route_data['obj'] = obj
451        request.route_data['name'] = name
452
453    def do_get(self, path, prop):
454        name = request.route_data['name']
455        return request.route_data['obj'][path][name]
456
457    def do_put(self, path, prop, value=None):
458        if value is None:
459            value = request.parameter_list
460
461        prop, iface, properties_iface = self.get_host_interface(
462            path, prop, request.route_data['map'][path])
463        try:
464            properties_iface.Set(iface, prop, value)
465        except ValueError, e:
466            abort(400, str(e))
467        except dbus.exceptions.DBusException, e:
468            if e.get_dbus_name() == DBUS_INVALID_ARGS:
469                bus_name = properties_iface.bus_name
470                expected_type = get_type_signature_by_introspection(self.bus,
471                                                                    bus_name,
472                                                                    path,
473                                                                    prop)
474                if not expected_type:
475                    abort(403, "Failed to get expected type: %s" % str(e))
476                converted_value = None
477                try:
478                    converted_value = convert_type(expected_type, value)
479                    self.do_put(path, prop, converted_value)
480                    return
481                except Exception as ex:
482                    abort(403, "Failed to convert %s to type %s" %
483                          (value, expected_type))
484                abort(403, str(e))
485            raise
486
487    def get_host_interface(self, path, prop, bus_info):
488        for bus, interfaces in bus_info.iteritems():
489            obj = self.bus.get_object(bus, path, introspect=True)
490            properties_iface = dbus.Interface(
491                obj, dbus_interface=dbus.PROPERTIES_IFACE)
492
493            info = self.get_host_interface_on_bus(
494                path, prop, properties_iface, bus, interfaces)
495            if info is not None:
496                prop, iface = info
497                return prop, iface, properties_iface
498
499    def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
500        for i in interfaces:
501            properties = self.try_properties_interface(iface.GetAll, i)
502            if not properties:
503                continue
504            match = obmc.utils.misc.find_case_insensitive(
505                prop, properties.keys())
506            if match is None:
507                continue
508            prop = match
509            return prop, i
510
511
512class SchemaHandler(RouteHandler):
513    verbs = ['GET']
514    rules = '<path:path>/schema'
515
516    def __init__(self, app, bus):
517        super(SchemaHandler, self).__init__(
518            app, bus, self.verbs, self.rules)
519
520    def find(self, path):
521        return self.try_mapper_call(
522            self.mapper.get_object,
523            path=path)
524
525    def setup(self, path):
526        request.route_data['map'] = self.find(path)
527
528    def do_get(self, path):
529        schema = {}
530        for x in request.route_data['map'].iterkeys():
531            obj = self.bus.get_object(x, path, introspect=False)
532            iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
533            data = iface.Introspect()
534            parser = IntrospectionNodeParser(
535                ElementTree.fromstring(data))
536            for x, y in parser.get_interfaces().iteritems():
537                schema[x] = y
538
539        return schema
540
541
542class InstanceHandler(RouteHandler):
543    verbs = ['GET', 'PUT', 'DELETE']
544    rules = '<path:path>'
545    request_type = dict
546
547    def __init__(self, app, bus):
548        super(InstanceHandler, self).__init__(
549            app, bus, self.verbs, self.rules)
550
551    def find(self, path, callback=None):
552        return {path: self.try_mapper_call(
553            self.mapper.get_object,
554            callback,
555            path=path)}
556
557    def setup(self, path):
558        callback = None
559        if request.method == 'PUT':
560            def callback(e, **kw):
561                abort(403, _4034_msg % ('resource', 'created', path))
562
563        if request.route_data.get('map') is None:
564            request.route_data['map'] = self.find(path, callback)
565
566    def do_get(self, path):
567        return self.mapper.enumerate_object(
568            path,
569            mapper_data=request.route_data['map'])
570
571    def do_put(self, path):
572        # make sure all properties exist in the request
573        obj = set(self.do_get(path).keys())
574        req = set(request.parameter_list.keys())
575
576        diff = list(obj.difference(req))
577        if diff:
578            abort(403, _4034_msg % (
579                'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
580
581        diff = list(req.difference(obj))
582        if diff:
583            abort(403, _4034_msg % (
584                'resource', 'created', '%s/attr/%s' % (path, diff[0])))
585
586        for p, v in request.parameter_list.iteritems():
587            self.app.property_handler.do_put(
588                path, p, v)
589
590    def do_delete(self, path):
591        for bus_info in request.route_data['map'][path].iteritems():
592            if self.bus_missing_delete(path, *bus_info):
593                abort(403, _4034_msg % ('resource', 'removed', path))
594
595        for bus in request.route_data['map'][path].iterkeys():
596            self.delete_on_bus(path, bus)
597
598    def bus_missing_delete(self, path, bus, interfaces):
599        return DELETE_IFACE not in interfaces
600
601    def delete_on_bus(self, path, bus):
602        obj = self.bus.get_object(bus, path, introspect=False)
603        delete_iface = dbus.Interface(
604            obj, dbus_interface=DELETE_IFACE)
605        delete_iface.Delete()
606
607
608class SessionHandler(MethodHandler):
609    ''' Handles the /login and /logout routes, manages
610    server side session store and session cookies.  '''
611
612    rules = ['/login', '/logout']
613    login_str = "User '%s' logged %s"
614    bad_passwd_str = "Invalid username or password"
615    no_user_str = "No user logged in"
616    bad_json_str = "Expecting request format { 'data': " \
617        "[<username>, <password>] }, got '%s'"
618    _require_auth = None
619    MAX_SESSIONS = 16
620
621    def __init__(self, app, bus):
622        super(SessionHandler, self).__init__(
623            app, bus)
624        self.hmac_key = os.urandom(128)
625        self.session_store = []
626
627    @staticmethod
628    def authenticate(username, clear):
629        try:
630            encoded = spwd.getspnam(username)[1]
631            return encoded == crypt.crypt(clear, encoded)
632        except KeyError:
633            return False
634
635    def invalidate_session(self, session):
636        try:
637            self.session_store.remove(session)
638        except ValueError:
639            pass
640
641    def new_session(self):
642        sid = os.urandom(32)
643        if self.MAX_SESSIONS <= len(self.session_store):
644            self.session_store.pop()
645        self.session_store.insert(0, {'sid': sid})
646
647        return self.session_store[0]
648
649    def get_session(self, sid):
650        sids = [x['sid'] for x in self.session_store]
651        try:
652            return self.session_store[sids.index(sid)]
653        except ValueError:
654            return None
655
656    def get_session_from_cookie(self):
657        return self.get_session(
658            request.get_cookie(
659                'sid', secret=self.hmac_key))
660
661    def do_post(self, **kw):
662        if request.path == '/login':
663            return self.do_login(**kw)
664        else:
665            return self.do_logout(**kw)
666
667    def do_logout(self, **kw):
668        session = self.get_session_from_cookie()
669        if session is not None:
670            user = session['user']
671            self.invalidate_session(session)
672            response.delete_cookie('sid')
673            return self.login_str % (user, 'out')
674
675        return self.no_user_str
676
677    def do_login(self, **kw):
678        session = self.get_session_from_cookie()
679        if session is not None:
680            return self.login_str % (session['user'], 'in')
681
682        if len(request.parameter_list) != 2:
683            abort(400, self.bad_json_str % (request.json))
684
685        if not self.authenticate(*request.parameter_list):
686            abort(401, self.bad_passwd_str)
687
688        user = request.parameter_list[0]
689        session = self.new_session()
690        session['user'] = user
691        response.set_cookie(
692            'sid', session['sid'], secret=self.hmac_key,
693            secure=True,
694            httponly=True)
695        return self.login_str % (user, 'in')
696
697    def find(self, **kw):
698        pass
699
700    def setup(self, **kw):
701        pass
702
703
704class ImageUploadUtils:
705    ''' Provides common utils for image upload. '''
706
707    file_loc = '/tmp/images'
708    file_prefix = 'img'
709    file_suffix = ''
710
711    @classmethod
712    def do_upload(cls, filename=''):
713        if not os.path.exists(cls.file_loc):
714            abort(500, "Error Directory not found")
715        if not filename:
716            handle, filename = tempfile.mkstemp(cls.file_suffix,
717                                                cls.file_prefix, cls.file_loc)
718        else:
719            filename = os.path.join(cls.file_loc, filename)
720            handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
721        try:
722            file_contents = request.body.read()
723            request.body.close()
724            os.write(handle, file_contents)
725        except (IOError, ValueError), e:
726            abort(400, str(e))
727        except:
728            abort(400, "Unexpected Error")
729        finally:
730            os.close(handle)
731
732
733class ImagePostHandler(RouteHandler):
734    ''' Handles the /upload/image route. '''
735
736    verbs = ['POST']
737    rules = ['/upload/image']
738    content_type = 'application/octet-stream'
739
740    def __init__(self, app, bus):
741        super(ImagePostHandler, self).__init__(
742            app, bus, self.verbs, self.rules, self.content_type)
743
744    def do_post(self, filename=''):
745        ImageUploadUtils.do_upload()
746
747    def find(self, **kw):
748        pass
749
750    def setup(self, **kw):
751        pass
752
753
754class EventNotifier:
755    keyNames = {}
756    keyNames['event'] = 'event'
757    keyNames['path'] = 'path'
758    keyNames['intfMap'] = 'interfaces'
759    keyNames['propMap'] = 'properties'
760    keyNames['intf'] = 'interface'
761
762    def __init__(self, wsock, filters):
763        self.wsock = wsock
764        self.paths = filters.get("paths", [])
765        self.interfaces = filters.get("interfaces", [])
766        if not self.paths:
767            self.paths.append(None)
768        bus = dbus.SystemBus()
769        # Add a signal receiver for every path the client is interested in
770        for path in self.paths:
771            bus.add_signal_receiver(
772                self.interfaces_added_handler,
773                dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
774                signal_name='InterfacesAdded',
775                path=path)
776            bus.add_signal_receiver(
777                self.properties_changed_handler,
778                dbus_interface=dbus.PROPERTIES_IFACE,
779                signal_name='PropertiesChanged',
780                path=path,
781                path_keyword='path')
782        loop = gobject.MainLoop()
783        # gobject's mainloop.run() will block the entire process, so the gevent
784        # scheduler and hence greenlets won't execute. The while-loop below
785        # works around this limitation by using gevent's sleep, instead of
786        # calling loop.run()
787        gcontext = loop.get_context()
788        while loop is not None:
789            try:
790                if gcontext.pending():
791                    gcontext.iteration()
792                else:
793                    # gevent.sleep puts only the current greenlet to sleep,
794                    # not the entire process.
795                    gevent.sleep(5)
796            except WebSocketError:
797                break
798
799    def interfaces_added_handler(self, path, iprops, **kw):
800        ''' If the client is interested in these changes, respond to the
801            client. This handles d-bus interface additions.'''
802        if (not self.interfaces) or \
803           (not set(iprops).isdisjoint(self.interfaces)):
804            response = {}
805            response[self.keyNames['event']] = "InterfacesAdded"
806            response[self.keyNames['path']] = path
807            response[self.keyNames['intfMap']] = iprops
808            try:
809                self.wsock.send(json.dumps(response))
810            except WebSocketError:
811                return
812
813    def properties_changed_handler(self, interface, new, old, **kw):
814        ''' If the client is interested in these changes, respond to the
815            client. This handles d-bus property changes. '''
816        if (not self.interfaces) or (interface in self.interfaces):
817            path = str(kw['path'])
818            response = {}
819            response[self.keyNames['event']] = "PropertiesChanged"
820            response[self.keyNames['path']] = path
821            response[self.keyNames['intf']] = interface
822            response[self.keyNames['propMap']] = new
823            try:
824                self.wsock.send(json.dumps(response))
825            except WebSocketError:
826                return
827
828
829class EventHandler(RouteHandler):
830    ''' Handles the /subscribe route, for clients to be able
831        to subscribe to BMC events. '''
832
833    verbs = ['GET']
834    rules = ['/subscribe']
835
836    def __init__(self, app, bus):
837        super(EventHandler, self).__init__(
838            app, bus, self.verbs, self.rules)
839
840    def find(self, **kw):
841        pass
842
843    def setup(self, **kw):
844        pass
845
846    def do_get(self):
847        wsock = request.environ.get('wsgi.websocket')
848        if not wsock:
849            abort(400, 'Expected WebSocket request.')
850        filters = wsock.receive()
851        filters = json.loads(filters)
852        notifier = EventNotifier(wsock, filters)
853
854
855class ImagePutHandler(RouteHandler):
856    ''' Handles the /upload/image/<filename> route. '''
857
858    verbs = ['PUT']
859    rules = ['/upload/image/<filename>']
860    content_type = 'application/octet-stream'
861
862    def __init__(self, app, bus):
863        super(ImagePutHandler, self).__init__(
864            app, bus, self.verbs, self.rules, self.content_type)
865
866    def do_put(self, filename=''):
867        ImageUploadUtils.do_upload(filename)
868
869    def find(self, **kw):
870        pass
871
872    def setup(self, **kw):
873        pass
874
875
876class DownloadDumpHandler(RouteHandler):
877    ''' Handles the /download/dump route. '''
878
879    verbs = 'GET'
880    rules = ['/download/dump/<dumpid>']
881    content_type = 'application/octet-stream'
882    dump_loc = '/var/lib/phosphor-debug-collector/dumps'
883    suppress_json_resp = True
884
885    def __init__(self, app, bus):
886        super(DownloadDumpHandler, self).__init__(
887            app, bus, self.verbs, self.rules, self.content_type)
888
889    def do_get(self, dumpid):
890        return self.do_download(dumpid)
891
892    def find(self, **kw):
893        pass
894
895    def setup(self, **kw):
896        pass
897
898    def do_download(self, dumpid):
899        dump_loc = os.path.join(self.dump_loc, dumpid)
900        if not os.path.exists(dump_loc):
901            abort(404, "Path not found")
902
903        files = os.listdir(dump_loc)
904        num_files = len(files)
905        if num_files == 0:
906            abort(404, "Dump not found")
907
908        return static_file(os.path.basename(files[0]), root=dump_loc,
909                           download=True, mimetype=self.content_type)
910
911
912class AuthorizationPlugin(object):
913    ''' Invokes an optional list of authorization callbacks. '''
914
915    name = 'authorization'
916    api = 2
917
918    class Compose:
919        def __init__(self, validators, callback, session_mgr):
920            self.validators = validators
921            self.callback = callback
922            self.session_mgr = session_mgr
923
924        def __call__(self, *a, **kw):
925            sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
926            session = self.session_mgr.get_session(sid)
927            if request.method != 'OPTIONS':
928                for x in self.validators:
929                    x(session, *a, **kw)
930
931            return self.callback(*a, **kw)
932
933    def apply(self, callback, route):
934        undecorated = route.get_undecorated_callback()
935        if not isinstance(undecorated, RouteHandler):
936            return callback
937
938        auth_types = getattr(
939            undecorated, '_require_auth', None)
940        if not auth_types:
941            return callback
942
943        return self.Compose(
944            auth_types, callback, undecorated.app.session_handler)
945
946
947class CorsPlugin(object):
948    ''' Add CORS headers. '''
949
950    name = 'cors'
951    api = 2
952
953    @staticmethod
954    def process_origin():
955        origin = request.headers.get('Origin')
956        if origin:
957            response.add_header('Access-Control-Allow-Origin', origin)
958            response.add_header(
959                'Access-Control-Allow-Credentials', 'true')
960
961    @staticmethod
962    def process_method_and_headers(verbs):
963        method = request.headers.get('Access-Control-Request-Method')
964        headers = request.headers.get('Access-Control-Request-Headers')
965        if headers:
966            headers = [x.lower() for x in headers.split(',')]
967
968        if method in verbs \
969                and headers == ['content-type']:
970            response.add_header('Access-Control-Allow-Methods', method)
971            response.add_header(
972                'Access-Control-Allow-Headers', 'Content-Type')
973
974    def __init__(self, app):
975        app.install_error_callback(self.error_callback)
976
977    def apply(self, callback, route):
978        undecorated = route.get_undecorated_callback()
979        if not isinstance(undecorated, RouteHandler):
980            return callback
981
982        if not getattr(undecorated, '_enable_cors', None):
983            return callback
984
985        def wrap(*a, **kw):
986            self.process_origin()
987            self.process_method_and_headers(undecorated._verbs)
988            return callback(*a, **kw)
989
990        return wrap
991
992    def error_callback(self, **kw):
993        self.process_origin()
994
995
996class JsonApiRequestPlugin(object):
997    ''' Ensures request content satisfies the OpenBMC json api format. '''
998    name = 'json_api_request'
999    api = 2
1000
1001    error_str = "Expecting request format { 'data': <value> }, got '%s'"
1002    type_error_str = "Unsupported Content-Type: '%s'"
1003    json_type = "application/json"
1004    request_methods = ['PUT', 'POST', 'PATCH']
1005
1006    @staticmethod
1007    def content_expected():
1008        return request.method in JsonApiRequestPlugin.request_methods
1009
1010    def validate_request(self):
1011        if request.content_length > 0 and \
1012                request.content_type != self.json_type:
1013            abort(415, self.type_error_str % request.content_type)
1014
1015        try:
1016            request.parameter_list = request.json.get('data')
1017        except ValueError, e:
1018            abort(400, str(e))
1019        except (AttributeError, KeyError, TypeError):
1020            abort(400, self.error_str % request.json)
1021
1022    def apply(self, callback, route):
1023        content_type = getattr(
1024            route.get_undecorated_callback(), '_content_type', None)
1025        if self.json_type != content_type:
1026            return callback
1027
1028        verbs = getattr(
1029            route.get_undecorated_callback(), '_verbs', None)
1030        if verbs is None:
1031            return callback
1032
1033        if not set(self.request_methods).intersection(verbs):
1034            return callback
1035
1036        def wrap(*a, **kw):
1037            if self.content_expected():
1038                self.validate_request()
1039            return callback(*a, **kw)
1040
1041        return wrap
1042
1043
1044class JsonApiRequestTypePlugin(object):
1045    ''' Ensures request content type satisfies the OpenBMC json api format. '''
1046    name = 'json_api_method_request'
1047    api = 2
1048
1049    error_str = "Expecting request format { 'data': %s }, got '%s'"
1050    json_type = "application/json"
1051
1052    def apply(self, callback, route):
1053        content_type = getattr(
1054            route.get_undecorated_callback(), '_content_type', None)
1055        if self.json_type != content_type:
1056            return callback
1057
1058        request_type = getattr(
1059            route.get_undecorated_callback(), 'request_type', None)
1060        if request_type is None:
1061            return callback
1062
1063        def validate_request():
1064            if not isinstance(request.parameter_list, request_type):
1065                abort(400, self.error_str % (str(request_type), request.json))
1066
1067        def wrap(*a, **kw):
1068            if JsonApiRequestPlugin.content_expected():
1069                validate_request()
1070            return callback(*a, **kw)
1071
1072        return wrap
1073
1074
1075class JsonErrorsPlugin(JSONPlugin):
1076    ''' Extend the Bottle JSONPlugin such that it also encodes error
1077        responses. '''
1078
1079    def __init__(self, app, **kw):
1080        super(JsonErrorsPlugin, self).__init__(**kw)
1081        self.json_opts = {
1082            x: y for x, y in kw.iteritems()
1083            if x in ['indent', 'sort_keys']}
1084        app.install_error_callback(self.error_callback)
1085
1086    def error_callback(self, response_object, response_body, **kw):
1087        response_body['body'] = json.dumps(response_object, **self.json_opts)
1088        response.content_type = 'application/json'
1089
1090
1091class JsonApiResponsePlugin(object):
1092    ''' Emits responses in the OpenBMC json api format. '''
1093    name = 'json_api_response'
1094    api = 2
1095
1096    @staticmethod
1097    def has_body():
1098        return request.method not in ['OPTIONS']
1099
1100    def __init__(self, app):
1101        app.install_error_callback(self.error_callback)
1102
1103    def apply(self, callback, route):
1104        skip = getattr(
1105            route.get_undecorated_callback(), 'suppress_json_resp', None)
1106        if skip:
1107            return callback
1108
1109        def wrap(*a, **kw):
1110            data = callback(*a, **kw)
1111            if self.has_body():
1112                resp = {'data': data}
1113                resp['status'] = 'ok'
1114                resp['message'] = response.status_line
1115                return resp
1116        return wrap
1117
1118    def error_callback(self, error, response_object, **kw):
1119        response_object['message'] = error.status_line
1120        response_object['status'] = 'error'
1121        response_object.setdefault('data', {})['description'] = str(error.body)
1122        if error.status_code == 500:
1123            response_object['data']['exception'] = repr(error.exception)
1124            response_object['data']['traceback'] = error.traceback.splitlines()
1125
1126
1127class JsonpPlugin(object):
1128    ''' Json javascript wrapper. '''
1129    name = 'jsonp'
1130    api = 2
1131
1132    def __init__(self, app, **kw):
1133        app.install_error_callback(self.error_callback)
1134
1135    @staticmethod
1136    def to_jsonp(json):
1137        jwrapper = request.query.callback or None
1138        if(jwrapper):
1139            response.set_header('Content-Type', 'application/javascript')
1140            json = jwrapper + '(' + json + ');'
1141        return json
1142
1143    def apply(self, callback, route):
1144        def wrap(*a, **kw):
1145            return self.to_jsonp(callback(*a, **kw))
1146        return wrap
1147
1148    def error_callback(self, response_body, **kw):
1149        response_body['body'] = self.to_jsonp(response_body['body'])
1150
1151
1152class ContentCheckerPlugin(object):
1153    ''' Ensures that a route is associated with the expected content-type
1154        header. '''
1155    name = 'content_checker'
1156    api = 2
1157
1158    class Checker:
1159        def __init__(self, type, callback):
1160            self.expected_type = type
1161            self.callback = callback
1162            self.error_str = "Expecting content type '%s', got '%s'"
1163
1164        def __call__(self, *a, **kw):
1165            if request.method in ['PUT', 'POST', 'PATCH'] and \
1166                    self.expected_type and \
1167                    self.expected_type != request.content_type:
1168                abort(415, self.error_str % (self.expected_type,
1169                      request.content_type))
1170
1171            return self.callback(*a, **kw)
1172
1173    def apply(self, callback, route):
1174        content_type = getattr(
1175            route.get_undecorated_callback(), '_content_type', None)
1176
1177        return self.Checker(content_type, callback)
1178
1179
1180class App(Bottle):
1181    def __init__(self, **kw):
1182        super(App, self).__init__(autojson=False)
1183
1184        self.have_wsock = kw.get('have_wsock', False)
1185
1186        self.bus = dbus.SystemBus()
1187        self.mapper = obmc.mapper.Mapper(self.bus)
1188        self.error_callbacks = []
1189
1190        self.install_hooks()
1191        self.install_plugins()
1192        self.create_handlers()
1193        self.install_handlers()
1194
1195    def install_plugins(self):
1196        # install json api plugins
1197        json_kw = {'indent': 2, 'sort_keys': True}
1198        self.install(AuthorizationPlugin())
1199        self.install(CorsPlugin(self))
1200        self.install(ContentCheckerPlugin())
1201        self.install(JsonpPlugin(self, **json_kw))
1202        self.install(JsonErrorsPlugin(self, **json_kw))
1203        self.install(JsonApiResponsePlugin(self))
1204        self.install(JsonApiRequestPlugin())
1205        self.install(JsonApiRequestTypePlugin())
1206
1207    def install_hooks(self):
1208        self.error_handler_type = type(self.default_error_handler)
1209        self.original_error_handler = self.default_error_handler
1210        self.default_error_handler = self.error_handler_type(
1211            self.custom_error_handler, self, Bottle)
1212
1213        self.real_router_match = self.router.match
1214        self.router.match = self.custom_router_match
1215        self.add_hook('before_request', self.strip_extra_slashes)
1216
1217    def create_handlers(self):
1218        # create route handlers
1219        self.session_handler = SessionHandler(self, self.bus)
1220        self.directory_handler = DirectoryHandler(self, self.bus)
1221        self.list_names_handler = ListNamesHandler(self, self.bus)
1222        self.list_handler = ListHandler(self, self.bus)
1223        self.method_handler = MethodHandler(self, self.bus)
1224        self.property_handler = PropertyHandler(self, self.bus)
1225        self.schema_handler = SchemaHandler(self, self.bus)
1226        self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1227        self.image_upload_put_handler = ImagePutHandler(self, self.bus)
1228        self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
1229        if self.have_wsock:
1230            self.event_handler = EventHandler(self, self.bus)
1231        self.instance_handler = InstanceHandler(self, self.bus)
1232
1233    def install_handlers(self):
1234        self.session_handler.install()
1235        self.directory_handler.install()
1236        self.list_names_handler.install()
1237        self.list_handler.install()
1238        self.method_handler.install()
1239        self.property_handler.install()
1240        self.schema_handler.install()
1241        self.image_upload_post_handler.install()
1242        self.image_upload_put_handler.install()
1243        self.download_dump_get_handler.install()
1244        if self.have_wsock:
1245            self.event_handler.install()
1246        # this has to come last, since it matches everything
1247        self.instance_handler.install()
1248
1249    def install_error_callback(self, callback):
1250        self.error_callbacks.insert(0, callback)
1251
1252    def custom_router_match(self, environ):
1253        ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1254            needed doesn't work for us since the instance rules match
1255            everything. This monkey-patch lets the route handler figure
1256            out which response is needed.  This could be accomplished
1257            with a hook but that would require calling the router match
1258            function twice.
1259        '''
1260        route, args = self.real_router_match(environ)
1261        if isinstance(route.callback, RouteHandler):
1262            route.callback._setup(**args)
1263
1264        return route, args
1265
1266    def custom_error_handler(self, res, error):
1267        ''' Allow plugins to modify error responses too via this custom
1268            error handler. '''
1269
1270        response_object = {}
1271        response_body = {}
1272        for x in self.error_callbacks:
1273            x(error=error,
1274                response_object=response_object,
1275                response_body=response_body)
1276
1277        return response_body.get('body', "")
1278
1279    @staticmethod
1280    def strip_extra_slashes():
1281        path = request.environ['PATH_INFO']
1282        trailing = ("", "/")[path[-1] == '/']
1283        parts = filter(bool, path.split('/'))
1284        request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
1285