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