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