blob: c6d29494426257c73e77f8712a66412302e537d1 [file] [log] [blame]
Brad Bishopaa65f6e2015-10-27 16:28:51 -04001#!/usr/bin/env python
2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05003import os
4import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -04005import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05006import dbus.exceptions
7import json
8import logging
9from xml.etree import ElementTree
10from rocket import Rocket
11from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopaa65f6e2015-10-27 16:28:51 -040012import OpenBMCMapper
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050013from OpenBMCMapper import Mapper, PathTree, IntrospectionNodeParser, ListMatch
Brad Bishop2f428582015-12-02 10:56:11 -050014import spwd
15import grp
16import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040017
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050018DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
19DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
20DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050021DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Brad Bishopaac521c2015-11-25 09:16:35 -050022DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050023
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050024_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040025
Brad Bishop2f428582015-12-02 10:56:11 -050026def valid_user(session, *a, **kw):
27 ''' Authorization plugin callback that checks that the user is logged in. '''
28 if session is None:
29 abort(403, 'Login required')
30
31class UserInGroup:
32 ''' Authorization plugin callback that checks that the user is logged in
33 and a member of a group. '''
34 def __init__(self, group):
35 self.group = group
36
37 def __call__(self, session, *a, **kw):
38 valid_user(session, *a, **kw)
39 res = False
40
41 try:
42 res = session['user'] in grp.getgrnam(self.group)[3]
43 except KeyError:
44 pass
45
46 if not res:
47 abort(403, 'Insufficient access')
48
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050049def find_case_insensitive(value, lst):
50 return next((x for x in lst if x.lower() == value.lower()), None)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040051
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050052def makelist(data):
53 if isinstance(data, list):
54 return data
55 elif data:
56 return [data]
57 else:
58 return []
Brad Bishopaa65f6e2015-10-27 16:28:51 -040059
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050060class RouteHandler(object):
Brad Bishop2f428582015-12-02 10:56:11 -050061 _require_auth = makelist(valid_user)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050062 def __init__(self, app, bus, verbs, rules):
63 self.app = app
64 self.bus = bus
65 self.mapper = Mapper(bus)
66 self._verbs = makelist(verbs)
67 self._rules = rules
Brad Bishopaa65f6e2015-10-27 16:28:51 -040068
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050069 def _setup(self, **kw):
70 request.route_data = {}
71 if request.method in self._verbs:
72 return self.setup(**kw)
73 else:
74 self.find(**kw)
75 raise HTTPError(405, "Method not allowed.",
76 Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040077
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050078 def __call__(self, **kw):
79 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040080
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050081 def install(self):
82 self.app.route(self._rules, callback = self,
83 method = ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -040084
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050085 @staticmethod
86 def try_mapper_call(f, callback = None, **kw):
Brad Bishopaa65f6e2015-10-27 16:28:51 -040087 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050088 return f(**kw)
89 except dbus.exceptions.DBusException, e:
90 if e.get_dbus_name() != OpenBMCMapper.MAPPER_NOT_FOUND:
91 raise
92 if callback is None:
93 def callback(e, **kw):
94 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040095
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050096 callback(e, **kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040097
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050098 @staticmethod
99 def try_properties_interface(f, *a):
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400100 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500101 return f(*a)
102 except dbus.exceptions.DBusException, e:
103 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
104 # interface doesn't have any properties
105 return None
106 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
107 # properties interface not implemented at all
108 return None
109 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400110
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500111class DirectoryHandler(RouteHandler):
112 verbs = 'GET'
113 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400114
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500115 def __init__(self, app, bus):
116 super(DirectoryHandler, self).__init__(
117 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400118
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500119 def find(self, path = '/'):
120 return self.try_mapper_call(
121 self.mapper.get_subtree_paths,
122 path = path, depth = 1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400123
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500124 def setup(self, path = '/'):
125 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400126
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500127 def do_get(self, path = '/'):
128 return request.route_data['map']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400129
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500130class ListNamesHandler(RouteHandler):
131 verbs = 'GET'
132 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400133
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500134 def __init__(self, app, bus):
135 super(ListNamesHandler, self).__init__(
136 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400137
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500138 def find(self, path = '/'):
139 return self.try_mapper_call(
140 self.mapper.get_subtree, path = path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400141
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500142 def setup(self, path = '/'):
143 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400144
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500145 def do_get(self, path = '/'):
146 return request.route_data['map']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400147
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500148class ListHandler(RouteHandler):
149 verbs = 'GET'
150 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400151
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500152 def __init__(self, app, bus):
153 super(ListHandler, self).__init__(
154 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400155
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500156 def find(self, path = '/'):
157 return self.try_mapper_call(
158 self.mapper.get_subtree, path = path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400159
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500160 def setup(self, path = '/'):
161 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400162
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500163 def do_get(self, path = '/'):
Brad Bishop936f5fe2015-11-03 15:10:11 -0500164 objs = {}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500165 mapper_data = request.route_data['map']
Brad Bishop936f5fe2015-11-03 15:10:11 -0500166 tree = PathTree()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400167 for x,y in mapper_data.iteritems():
Brad Bishop936f5fe2015-11-03 15:10:11 -0500168 tree[x] = y
169
170 try:
171 # Check to see if the root path implements
172 # enumerate in addition to any sub tree
173 # objects.
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500174 root = self.try_mapper_call(self.mapper.get_object,
175 path = path)
176 mapper_data[path] = root
Brad Bishop936f5fe2015-11-03 15:10:11 -0500177 except:
178 pass
179
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500180 have_enumerate = [ (x[0], self.enumerate_capable(*x)) \
181 for x in mapper_data.iteritems() \
182 if self.enumerate_capable(*x) ]
Brad Bishop936f5fe2015-11-03 15:10:11 -0500183
184 for x,y in have_enumerate:
185 objs.update(self.call_enumerate(x, y))
186 tmp = tree[x]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500187 # remove the subtree
Brad Bishop936f5fe2015-11-03 15:10:11 -0500188 del tree[x]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500189 # add the new leaf back since enumerate results don't
190 # include the object enumerate is being invoked on
Brad Bishop936f5fe2015-11-03 15:10:11 -0500191 tree[x] = tmp
192
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500193 # make dbus calls for any remaining objects
Brad Bishop936f5fe2015-11-03 15:10:11 -0500194 for x,y in tree.dataitems():
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500195 objs[x] = self.app.instance_handler.do_get(x)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400196
197 return objs
198
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500199 @staticmethod
200 def enumerate_capable(path, bus_data):
201 busses = []
202 for name, ifaces in bus_data.iteritems():
203 if OpenBMCMapper.ENUMERATE_IFACE in ifaces:
204 busses.append(name)
205 return busses
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400206
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500207 def call_enumerate(self, path, busses):
208 objs = {}
209 for b in busses:
210 obj = self.bus.get_object(b, path, introspect = False)
211 iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE)
212 objs.update(iface.enumerate())
213 return objs
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400214
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500215class MethodHandler(RouteHandler):
216 verbs = 'POST'
217 rules = '<path:path>/action/<method>'
218 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400219
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500220 def __init__(self, app, bus):
221 super(MethodHandler, self).__init__(
222 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400223
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500224 def find(self, path, method):
225 busses = self.try_mapper_call(self.mapper.get_object,
226 path = path)
227 for items in busses.iteritems():
228 m = self.find_method_on_bus(path, method, *items)
229 if m:
230 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400231
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500232 abort(404, _4034_msg %('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400233
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500234 def setup(self, path, method):
235 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400236
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500237 def do_post(self, path, method):
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400238 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500239 if request.parameter_list:
240 return request.route_data['method'](*request.parameter_list)
241 else:
242 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400243
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500244 except dbus.exceptions.DBusException, e:
245 if e.get_dbus_name() == DBUS_INVALID_ARGS:
246 abort(400, str(e))
Brad Bishopd4578922015-12-02 11:10:36 -0500247 if e.get_dbus_name() == DBUS_TYPE_ERROR:
248 abort(400, str(e))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500249 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400250
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500251 @staticmethod
252 def find_method_in_interface(method, obj, interface, methods):
253 if methods is None:
254 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400255
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500256 method = find_case_insensitive(method, methods.keys())
257 if method is not None:
258 iface = dbus.Interface(obj, interface)
259 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400260
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500261 def find_method_on_bus(self, path, method, bus, interfaces):
262 obj = self.bus.get_object(bus, path, introspect = False)
263 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
264 data = iface.Introspect()
265 parser = IntrospectionNodeParser(
266 ElementTree.fromstring(data),
267 intf_match = ListMatch(interfaces))
268 for x,y in parser.get_interfaces().iteritems():
269 m = self.find_method_in_interface(method, obj, x,
270 y.get('method'))
271 if m:
272 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500274class PropertyHandler(RouteHandler):
275 verbs = ['PUT', 'GET']
276 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400277
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500278 def __init__(self, app, bus):
279 super(PropertyHandler, self).__init__(
280 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400281
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500282 def find(self, path, prop):
283 self.app.instance_handler.setup(path)
284 obj = self.app.instance_handler.do_get(path)
285 try:
286 obj[prop]
287 except KeyError, e:
288 if request.method == 'PUT':
289 abort(403, _4034_msg %('property', 'created', str(e)))
290 else:
291 abort(404, _4034_msg %('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400292
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500293 return { path: obj }
294
295 def setup(self, path, prop):
296 request.route_data['obj'] = self.find(path, prop)
297
298 def do_get(self, path, prop):
299 return request.route_data['obj'][path][prop]
300
301 def do_put(self, path, prop, value = None):
302 if value is None:
303 value = request.parameter_list
304
305 prop, iface, properties_iface = self.get_host_interface(
306 path, prop, request.route_data['map'][path])
307 try:
308 properties_iface.Set(iface, prop, value)
309 except ValueError, e:
310 abort(400, str(e))
311 except dbus.exceptions.DBusException, e:
312 if e.get_dbus_name() == DBUS_INVALID_ARGS:
313 abort(403, str(e))
314 raise
315
316 def get_host_interface(self, path, prop, bus_info):
317 for bus, interfaces in bus_info.iteritems():
318 obj = self.bus.get_object(bus, path, introspect = True)
319 properties_iface = dbus.Interface(
320 obj, dbus_interface=dbus.PROPERTIES_IFACE)
321
322 info = self.get_host_interface_on_bus(
323 path, prop, properties_iface,
324 bus, interfaces)
325 if info is not None:
326 prop, iface = info
327 return prop, iface, properties_iface
328
329 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
330 for i in interfaces:
331 properties = self.try_properties_interface(iface.GetAll, i)
332 if properties is None:
333 continue
334 prop = find_case_insensitive(prop, properties.keys())
335 if prop is None:
336 continue
337 return prop, i
338
Brad Bishop2503bd62015-12-16 17:56:12 -0500339class SchemaHandler(RouteHandler):
340 verbs = ['GET']
341 rules = '<path:path>/schema'
342
343 def __init__(self, app, bus):
344 super(SchemaHandler, self).__init__(
345 app, bus, self.verbs, self.rules)
346
347 def find(self, path):
348 return self.try_mapper_call(
349 self.mapper.get_object,
350 path = path)
351
352 def setup(self, path):
353 request.route_data['map'] = self.find(path)
354
355 def do_get(self, path):
356 schema = {}
357 for x in request.route_data['map'].iterkeys():
358 obj = self.bus.get_object(
359 x, path, introspect = False)
360 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
361 data = iface.Introspect()
362 parser = IntrospectionNodeParser(
363 ElementTree.fromstring(data))
364 for x,y in parser.get_interfaces().iteritems():
365 schema[x] = y
366
367 return schema
368
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500369class InstanceHandler(RouteHandler):
370 verbs = ['GET', 'PUT', 'DELETE']
371 rules = '<path:path>'
372 request_type = dict
373
374 def __init__(self, app, bus):
375 super(InstanceHandler, self).__init__(
376 app, bus, self.verbs, self.rules)
377
378 def find(self, path, callback = None):
379 return { path: self.try_mapper_call(
380 self.mapper.get_object,
381 callback,
382 path = path) }
383
384 def setup(self, path):
385 callback = None
386 if request.method == 'PUT':
387 def callback(e, **kw):
388 abort(403, _4034_msg %('resource',
389 'created', path))
390
391 if request.route_data.get('map') is None:
392 request.route_data['map'] = self.find(path, callback)
393
394 def do_get(self, path):
395 properties = {}
396 for item in request.route_data['map'][path].iteritems():
397 properties.update(self.get_properties_on_bus(
398 path, *item))
399
400 return properties
401
402 @staticmethod
403 def get_properties_on_iface(properties_iface, iface):
404 properties = InstanceHandler.try_properties_interface(
405 properties_iface.GetAll, iface)
406 if properties is None:
407 return {}
408 return properties
409
410 def get_properties_on_bus(self, path, bus, interfaces):
411 properties = {}
412 obj = self.bus.get_object(bus, path, introspect = False)
413 properties_iface = dbus.Interface(
414 obj, dbus_interface=dbus.PROPERTIES_IFACE)
415 for i in interfaces:
416 properties.update(self.get_properties_on_iface(
417 properties_iface, i))
418
419 return properties
420
421 def do_put(self, path):
422 # make sure all properties exist in the request
423 obj = set(self.do_get(path).keys())
424 req = set(request.parameter_list.keys())
425
426 diff = list(obj.difference(req))
427 if diff:
428 abort(403, _4034_msg %('resource', 'removed',
429 '%s/attr/%s' %(path, diff[0])))
430
431 diff = list(req.difference(obj))
432 if diff:
433 abort(403, _4034_msg %('resource', 'created',
434 '%s/attr/%s' %(path, diff[0])))
435
436 for p,v in request.parameter_list.iteritems():
437 self.app.property_handler.do_put(
438 path, p, v)
439
440 def do_delete(self, path):
441 for bus_info in request.route_data['map'][path].iteritems():
442 if self.bus_missing_delete(path, *bus_info):
443 abort(403, _4034_msg %('resource', 'removed',
444 path))
445
446 for bus in request.route_data['map'][path].iterkeys():
447 self.delete_on_bus(path, bus)
448
449 def bus_missing_delete(self, path, bus, interfaces):
450 return DELETE_IFACE not in interfaces
451
452 def delete_on_bus(self, path, bus):
453 obj = self.bus.get_object(bus, path, introspect = False)
454 delete_iface = dbus.Interface(
455 obj, dbus_interface = DELETE_IFACE)
456 delete_iface.Delete()
457
Brad Bishop2f428582015-12-02 10:56:11 -0500458class SessionHandler(MethodHandler):
459 ''' Handles the /login and /logout routes, manages server side session store and
460 session cookies. '''
461
462 rules = ['/login', '/logout']
463 login_str = "User '%s' logged %s"
464 bad_passwd_str = "Invalid username or password"
465 no_user_str = "No user logged in"
466 bad_json_str = "Expecting request format { 'data': [<username>, <password>] }, got '%s'"
467 _require_auth = None
468 MAX_SESSIONS = 16
469
470 def __init__(self, app, bus):
471 super(SessionHandler, self).__init__(
472 app, bus)
473 self.hmac_key = os.urandom(128)
474 self.session_store = []
475
476 @staticmethod
477 def authenticate(username, clear):
478 try:
479 encoded = spwd.getspnam(username)[1]
480 return encoded == crypt.crypt(clear, encoded)
481 except KeyError:
482 return False
483
484 def invalidate_session(self, session):
485 try:
486 self.session_store.remove(session)
487 except ValueError:
488 pass
489
490 def new_session(self):
491 sid = os.urandom(32)
492 if self.MAX_SESSIONS <= len(self.session_store):
493 self.session_store.pop()
494 self.session_store.insert(0, {'sid': sid})
495
496 return self.session_store[0]
497
498 def get_session(self, sid):
499 sids = [ x['sid'] for x in self.session_store ]
500 try:
501 return self.session_store[sids.index(sid)]
502 except ValueError:
503 return None
504
505 def get_session_from_cookie(self):
506 return self.get_session(
507 request.get_cookie('sid',
508 secret = self.hmac_key))
509
510 def do_post(self, **kw):
511 if request.path == '/login':
512 return self.do_login(**kw)
513 else:
514 return self.do_logout(**kw)
515
516 def do_logout(self, **kw):
517 session = self.get_session_from_cookie()
518 if session is not None:
519 user = session['user']
520 self.invalidate_session(session)
521 response.delete_cookie('sid')
522 return self.login_str %(user, 'out')
523
524 return self.no_user_str
525
526 def do_login(self, **kw):
527 session = self.get_session_from_cookie()
528 if session is not None:
529 return self.login_str %(session['user'], 'in')
530
531 if len(request.parameter_list) != 2:
532 abort(400, self.bad_json_str %(request.json))
533
534 if not self.authenticate(*request.parameter_list):
535 return self.bad_passwd_str
536
537 user = request.parameter_list[0]
538 session = self.new_session()
539 session['user'] = user
540 response.set_cookie('sid', session['sid'], secret = self.hmac_key,
541 secure = True,
542 httponly = True)
543 return self.login_str %(user, 'in')
544
545 def find(self, **kw):
546 pass
547
548 def setup(self, **kw):
549 pass
550
551class AuthorizationPlugin(object):
552 ''' Invokes an optional list of authorization callbacks. '''
553
554 name = 'authorization'
555 api = 2
556
557 class Compose:
558 def __init__(self, validators, callback, session_mgr):
559 self.validators = validators
560 self.callback = callback
561 self.session_mgr = session_mgr
562
563 def __call__(self, *a, **kw):
564 sid = request.get_cookie('sid', secret = self.session_mgr.hmac_key)
565 session = self.session_mgr.get_session(sid)
566 for x in self.validators:
567 x(session, *a, **kw)
568
569 return self.callback(*a, **kw)
570
571 def apply(self, callback, route):
572 undecorated = route.get_undecorated_callback()
573 if not isinstance(undecorated, RouteHandler):
574 return callback
575
576 auth_types = getattr(undecorated,
577 '_require_auth', None)
578 if not auth_types:
579 return callback
580
581 return self.Compose(auth_types, callback,
582 undecorated.app.session_handler)
583
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500584class JsonApiRequestPlugin(object):
585 ''' Ensures request content satisfies the OpenBMC json api format. '''
586 name = 'json_api_request'
587 api = 2
588
589 error_str = "Expecting request format { 'data': <value> }, got '%s'"
590 type_error_str = "Unsupported Content-Type: '%s'"
591 json_type = "application/json"
592 request_methods = ['PUT', 'POST', 'PATCH']
593
594 @staticmethod
595 def content_expected():
596 return request.method in JsonApiRequestPlugin.request_methods
597
598 def validate_request(self):
599 if request.content_length > 0 and \
600 request.content_type != self.json_type:
601 abort(415, self.type_error_str %(request.content_type))
602
603 try:
604 request.parameter_list = request.json.get('data')
605 except ValueError, e:
606 abort(400, str(e))
607 except (AttributeError, KeyError, TypeError):
608 abort(400, self.error_str %(request.json))
609
610 def apply(self, callback, route):
611 verbs = getattr(route.get_undecorated_callback(),
612 '_verbs', None)
613 if verbs is None:
614 return callback
615
616 if not set(self.request_methods).intersection(verbs):
617 return callback
618
619 def wrap(*a, **kw):
620 if self.content_expected():
621 self.validate_request()
622 return callback(*a, **kw)
623
624 return wrap
625
626class JsonApiRequestTypePlugin(object):
627 ''' Ensures request content type satisfies the OpenBMC json api format. '''
628 name = 'json_api_method_request'
629 api = 2
630
631 error_str = "Expecting request format { 'data': %s }, got '%s'"
632
633 def apply(self, callback, route):
634 request_type = getattr(route.get_undecorated_callback(),
635 'request_type', None)
636 if request_type is None:
637 return callback
638
639 def validate_request():
640 if not isinstance(request.parameter_list, request_type):
641 abort(400, self.error_str %(str(request_type), request.json))
642
643 def wrap(*a, **kw):
644 if JsonApiRequestPlugin.content_expected():
645 validate_request()
646 return callback(*a, **kw)
647
648 return wrap
649
650class JsonApiResponsePlugin(object):
651 ''' Emits normal responses in the OpenBMC json api format. '''
652 name = 'json_api_response'
653 api = 2
654
655 def apply(self, callback, route):
656 def wrap(*a, **kw):
657 resp = { 'data': callback(*a, **kw) }
658 resp['status'] = 'ok'
659 resp['message'] = response.status_line
660 return resp
661 return wrap
662
663class JsonApiErrorsPlugin(object):
664 ''' Emits error responses in the OpenBMC json api format. '''
665 name = 'json_api_errors'
666 api = 2
667
668 def __init__(self, **kw):
669 self.app = None
670 self.function_type = None
671 self.original = None
672 self.json_opts = { x:y for x,y in kw.iteritems() \
673 if x in ['indent','sort_keys'] }
674
675 def setup(self, app):
676 self.app = app
677 self.function_type = type(app.default_error_handler)
678 self.original = app.default_error_handler
679 self.app.default_error_handler = self.function_type(
680 self.json_errors, app, Bottle)
681
682 def apply(self, callback, route):
683 return callback
684
685 def close(self):
686 self.app.default_error_handler = self.function_type(
687 self.original, self.app, Bottle)
688
689 def json_errors(self, res, error):
690 response_object = {'status': 'error', 'data': {} }
691 response_object['message'] = error.status_line
692 response_object['data']['description'] = str(error.body)
693 if error.status_code == 500:
694 response_object['data']['exception'] = repr(error.exception)
695 response_object['data']['traceback'] = error.traceback.splitlines()
696
697 json_response = json.dumps(response_object, **self.json_opts)
Brad Bishop9bfeec22015-11-17 09:14:50 -0500698 response.content_type = 'application/json'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500699 return json_response
700
701class RestApp(Bottle):
702 def __init__(self, bus):
703 super(RestApp, self).__init__(autojson = False)
Brad Bishop53fd4932015-10-30 09:22:30 -0400704 self.bus = bus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500705 self.mapper = Mapper(bus)
706
707 self.install_hooks()
708 self.install_plugins()
709 self.create_handlers()
710 self.install_handlers()
711
712 def install_plugins(self):
713 # install json api plugins
714 json_kw = {'indent': 2, 'sort_keys': True}
715 self.install(JSONPlugin(**json_kw))
716 self.install(JsonApiErrorsPlugin(**json_kw))
Brad Bishop2f428582015-12-02 10:56:11 -0500717 self.install(AuthorizationPlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500718 self.install(JsonApiResponsePlugin())
719 self.install(JsonApiRequestPlugin())
720 self.install(JsonApiRequestTypePlugin())
721
722 def install_hooks(self):
723 self.real_router_match = self.router.match
724 self.router.match = self.custom_router_match
725 self.add_hook('before_request', self.strip_extra_slashes)
726
727 def create_handlers(self):
728 # create route handlers
Brad Bishop2f428582015-12-02 10:56:11 -0500729 self.session_handler = SessionHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500730 self.directory_handler = DirectoryHandler(self, self.bus)
731 self.list_names_handler = ListNamesHandler(self, self.bus)
732 self.list_handler = ListHandler(self, self.bus)
733 self.method_handler = MethodHandler(self, self.bus)
734 self.property_handler = PropertyHandler(self, self.bus)
Brad Bishop2503bd62015-12-16 17:56:12 -0500735 self.schema_handler = SchemaHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500736 self.instance_handler = InstanceHandler(self, self.bus)
737
738 def install_handlers(self):
Brad Bishop2f428582015-12-02 10:56:11 -0500739 self.session_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500740 self.directory_handler.install()
741 self.list_names_handler.install()
742 self.list_handler.install()
743 self.method_handler.install()
744 self.property_handler.install()
Brad Bishop2503bd62015-12-16 17:56:12 -0500745 self.schema_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500746 # this has to come last, since it matches everything
747 self.instance_handler.install()
748
749 def custom_router_match(self, environ):
750 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
751 needed doesn't work for us since the instance rules match everything.
752 This monkey-patch lets the route handler figure out which response is
753 needed. This could be accomplished with a hook but that would require
754 calling the router match function twice.
755 '''
756 route, args = self.real_router_match(environ)
757 if isinstance(route.callback, RouteHandler):
758 route.callback._setup(**args)
759
760 return route, args
761
762 @staticmethod
763 def strip_extra_slashes():
764 path = request.environ['PATH_INFO']
765 trailing = ("","/")[path[-1] == '/']
766 parts = filter(bool, path.split('/'))
767 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400768
769if __name__ == '__main__':
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500770 log = logging.getLogger('Rocket.Errors')
771 log.setLevel(logging.INFO)
772 log.addHandler(logging.StreamHandler(sys.stdout))
773
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400774 bus = dbus.SystemBus()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500775 app = RestApp(bus)
776 default_cert = os.path.join(sys.prefix, 'share',
777 os.path.basename(__file__), 'cert.pem')
778
779 server = Rocket(('0.0.0.0',
780 443,
781 default_cert,
782 default_cert),
Brad Bishopb7f756c2015-12-02 11:13:20 -0500783 'wsgi', {'wsgi_app': app},
784 min_threads = 1,
785 max_threads = 1)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500786 server.start()