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