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
970    def __init__(self, app):
971        app.install_error_callback(self.error_callback)
972
973    def apply(self, callback, route):
974        undecorated = route.get_undecorated_callback()
975        if not isinstance(undecorated, RouteHandler):
976            return callback
977
978        if not getattr(undecorated, '_enable_cors', None):
979            return callback
980
981        def wrap(*a, **kw):
982            self.process_origin()
983            self.process_method_and_headers(undecorated._verbs)
984            return callback(*a, **kw)
985
986        return wrap
987
988    def error_callback(self, **kw):
989        self.process_origin()
990
991
992class JsonApiRequestPlugin(object):
993    ''' Ensures request content satisfies the OpenBMC json api format. '''
994    name = 'json_api_request'
995    api = 2
996
997    error_str = "Expecting request format { 'data': <value> }, got '%s'"
998    type_error_str = "Unsupported Content-Type: '%s'"
999    json_type = "application/json"
1000    request_methods = ['PUT', 'POST', 'PATCH']
1001
1002    @staticmethod
1003    def content_expected():
1004        return request.method in JsonApiRequestPlugin.request_methods
1005
1006    def validate_request(self):
1007        if request.content_length > 0 and \
1008                request.content_type != self.json_type:
1009            abort(415, self.type_error_str % request.content_type)
1010
1011        try:
1012            request.parameter_list = request.json.get('data')
1013        except ValueError, e:
1014            abort(400, str(e))
1015        except (AttributeError, KeyError, TypeError):
1016            abort(400, self.error_str % request.json)
1017
1018    def apply(self, callback, route):
1019        content_type = getattr(
1020            route.get_undecorated_callback(), '_content_type', None)
1021        if self.json_type != content_type:
1022            return callback
1023
1024        verbs = getattr(
1025            route.get_undecorated_callback(), '_verbs', None)
1026        if verbs is None:
1027            return callback
1028
1029        if not set(self.request_methods).intersection(verbs):
1030            return callback
1031
1032        def wrap(*a, **kw):
1033            if self.content_expected():
1034                self.validate_request()
1035            return callback(*a, **kw)
1036
1037        return wrap
1038
1039
1040class JsonApiRequestTypePlugin(object):
1041    ''' Ensures request content type satisfies the OpenBMC json api format. '''
1042    name = 'json_api_method_request'
1043    api = 2
1044
1045    error_str = "Expecting request format { 'data': %s }, got '%s'"
1046    json_type = "application/json"
1047
1048    def apply(self, callback, route):
1049        content_type = getattr(
1050            route.get_undecorated_callback(), '_content_type', None)
1051        if self.json_type != content_type:
1052            return callback
1053
1054        request_type = getattr(
1055            route.get_undecorated_callback(), 'request_type', None)
1056        if request_type is None:
1057            return callback
1058
1059        def validate_request():
1060            if not isinstance(request.parameter_list, request_type):
1061                abort(400, self.error_str % (str(request_type), request.json))
1062
1063        def wrap(*a, **kw):
1064            if JsonApiRequestPlugin.content_expected():
1065                validate_request()
1066            return callback(*a, **kw)
1067
1068        return wrap
1069
1070
1071class JsonErrorsPlugin(JSONPlugin):
1072    ''' Extend the Bottle JSONPlugin such that it also encodes error
1073        responses. '''
1074
1075    def __init__(self, app, **kw):
1076        super(JsonErrorsPlugin, self).__init__(**kw)
1077        self.json_opts = {
1078            x: y for x, y in kw.iteritems()
1079            if x in ['indent', 'sort_keys']}
1080        app.install_error_callback(self.error_callback)
1081
1082    def error_callback(self, response_object, response_body, **kw):
1083        response_body['body'] = json.dumps(response_object, **self.json_opts)
1084        response.content_type = 'application/json'
1085
1086
1087class JsonApiResponsePlugin(object):
1088    ''' Emits responses in the OpenBMC json api format. '''
1089    name = 'json_api_response'
1090    api = 2
1091
1092    @staticmethod
1093    def has_body():
1094        return request.method not in ['OPTIONS']
1095
1096    def __init__(self, app):
1097        app.install_error_callback(self.error_callback)
1098
1099    def apply(self, callback, route):
1100        skip = getattr(
1101            route.get_undecorated_callback(), 'suppress_json_resp', None)
1102        if skip:
1103            return callback
1104
1105        def wrap(*a, **kw):
1106            data = callback(*a, **kw)
1107            if self.has_body():
1108                resp = {'data': data}
1109                resp['status'] = 'ok'
1110                resp['message'] = response.status_line
1111                return resp
1112        return wrap
1113
1114    def error_callback(self, error, response_object, **kw):
1115        response_object['message'] = error.status_line
1116        response_object['status'] = 'error'
1117        response_object.setdefault('data', {})['description'] = str(error.body)
1118        if error.status_code == 500:
1119            response_object['data']['exception'] = repr(error.exception)
1120            response_object['data']['traceback'] = error.traceback.splitlines()
1121
1122
1123class JsonpPlugin(object):
1124    ''' Json javascript wrapper. '''
1125    name = 'jsonp'
1126    api = 2
1127
1128    def __init__(self, app, **kw):
1129        app.install_error_callback(self.error_callback)
1130
1131    @staticmethod
1132    def to_jsonp(json):
1133        jwrapper = request.query.callback or None
1134        if(jwrapper):
1135            response.set_header('Content-Type', 'application/javascript')
1136            json = jwrapper + '(' + json + ');'
1137        return json
1138
1139    def apply(self, callback, route):
1140        def wrap(*a, **kw):
1141            return self.to_jsonp(callback(*a, **kw))
1142        return wrap
1143
1144    def error_callback(self, response_body, **kw):
1145        response_body['body'] = self.to_jsonp(response_body['body'])
1146
1147
1148class ContentCheckerPlugin(object):
1149    ''' Ensures that a route is associated with the expected content-type
1150        header. '''
1151    name = 'content_checker'
1152    api = 2
1153
1154    class Checker:
1155        def __init__(self, type, callback):
1156            self.expected_type = type
1157            self.callback = callback
1158            self.error_str = "Expecting content type '%s', got '%s'"
1159
1160        def __call__(self, *a, **kw):
1161            if request.method in ['PUT', 'POST', 'PATCH'] and \
1162                    self.expected_type and \
1163                    self.expected_type != request.content_type:
1164                abort(415, self.error_str % (self.expected_type,
1165                      request.content_type))
1166
1167            return self.callback(*a, **kw)
1168
1169    def apply(self, callback, route):
1170        content_type = getattr(
1171            route.get_undecorated_callback(), '_content_type', None)
1172
1173        return self.Checker(content_type, callback)
1174
1175
1176class App(Bottle):
1177    def __init__(self, **kw):
1178        super(App, self).__init__(autojson=False)
1179
1180        self.have_wsock = kw.get('have_wsock', False)
1181
1182        self.bus = dbus.SystemBus()
1183        self.mapper = obmc.mapper.Mapper(self.bus)
1184        self.error_callbacks = []
1185
1186        self.install_hooks()
1187        self.install_plugins()
1188        self.create_handlers()
1189        self.install_handlers()
1190
1191    def install_plugins(self):
1192        # install json api plugins
1193        json_kw = {'indent': 2, 'sort_keys': True}
1194        self.install(AuthorizationPlugin())
1195        self.install(CorsPlugin(self))
1196        self.install(ContentCheckerPlugin())
1197        self.install(JsonpPlugin(self, **json_kw))
1198        self.install(JsonErrorsPlugin(self, **json_kw))
1199        self.install(JsonApiResponsePlugin(self))
1200        self.install(JsonApiRequestPlugin())
1201        self.install(JsonApiRequestTypePlugin())
1202
1203    def install_hooks(self):
1204        self.error_handler_type = type(self.default_error_handler)
1205        self.original_error_handler = self.default_error_handler
1206        self.default_error_handler = self.error_handler_type(
1207            self.custom_error_handler, self, Bottle)
1208
1209        self.real_router_match = self.router.match
1210        self.router.match = self.custom_router_match
1211        self.add_hook('before_request', self.strip_extra_slashes)
1212
1213    def create_handlers(self):
1214        # create route handlers
1215        self.session_handler = SessionHandler(self, self.bus)
1216        self.directory_handler = DirectoryHandler(self, self.bus)
1217        self.list_names_handler = ListNamesHandler(self, self.bus)
1218        self.list_handler = ListHandler(self, self.bus)
1219        self.method_handler = MethodHandler(self, self.bus)
1220        self.property_handler = PropertyHandler(self, self.bus)
1221        self.schema_handler = SchemaHandler(self, self.bus)
1222        self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1223        self.image_upload_put_handler = ImagePutHandler(self, self.bus)
1224        self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
1225        if self.have_wsock:
1226            self.event_handler = EventHandler(self, self.bus)
1227        self.instance_handler = InstanceHandler(self, self.bus)
1228
1229    def install_handlers(self):
1230        self.session_handler.install()
1231        self.directory_handler.install()
1232        self.list_names_handler.install()
1233        self.list_handler.install()
1234        self.method_handler.install()
1235        self.property_handler.install()
1236        self.schema_handler.install()
1237        self.image_upload_post_handler.install()
1238        self.image_upload_put_handler.install()
1239        self.download_dump_get_handler.install()
1240        if self.have_wsock:
1241            self.event_handler.install()
1242        # this has to come last, since it matches everything
1243        self.instance_handler.install()
1244
1245    def install_error_callback(self, callback):
1246        self.error_callbacks.insert(0, callback)
1247
1248    def custom_router_match(self, environ):
1249        ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1250            needed doesn't work for us since the instance rules match
1251            everything. This monkey-patch lets the route handler figure
1252            out which response is needed.  This could be accomplished
1253            with a hook but that would require calling the router match
1254            function twice.
1255        '''
1256        route, args = self.real_router_match(environ)
1257        if isinstance(route.callback, RouteHandler):
1258            route.callback._setup(**args)
1259
1260        return route, args
1261
1262    def custom_error_handler(self, res, error):
1263        ''' Allow plugins to modify error responses too via this custom
1264            error handler. '''
1265
1266        response_object = {}
1267        response_body = {}
1268        for x in self.error_callbacks:
1269            x(error=error,
1270                response_object=response_object,
1271                response_body=response_body)
1272
1273        return response_body.get('body', "")
1274
1275    @staticmethod
1276    def strip_extra_slashes():
1277        path = request.environ['PATH_INFO']
1278        trailing = ("", "/")[path[-1] == '/']
1279        parts = filter(bool, path.split('/'))
1280        request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
1281