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