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