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