blob: 42b3a7481bf34da641f6cd508dba7c2b88dd6197 [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 Bishopaac521c2015-11-25 09:16:35 -050021DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050022
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050023_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040024
Brad Bishop2f428582015-12-02 10:56:11 -050025def valid_user(session, *a, **kw):
26 ''' Authorization plugin callback that checks that the user is logged in. '''
27 if session is None:
28 abort(403, 'Login required')
29
30class UserInGroup:
31 ''' Authorization plugin callback that checks that the user is logged in
32 and a member of a group. '''
33 def __init__(self, group):
34 self.group = group
35
36 def __call__(self, session, *a, **kw):
37 valid_user(session, *a, **kw)
38 res = False
39
40 try:
41 res = session['user'] in grp.getgrnam(self.group)[3]
42 except KeyError:
43 pass
44
45 if not res:
46 abort(403, 'Insufficient access')
47
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050048def find_case_insensitive(value, lst):
49 return next((x for x in lst if x.lower() == value.lower()), None)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040050
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050051def makelist(data):
52 if isinstance(data, list):
53 return data
54 elif data:
55 return [data]
56 else:
57 return []
Brad Bishopaa65f6e2015-10-27 16:28:51 -040058
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050059class RouteHandler(object):
Brad Bishop2f428582015-12-02 10:56:11 -050060 _require_auth = makelist(valid_user)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050061 def __init__(self, app, bus, verbs, rules):
62 self.app = app
63 self.bus = bus
64 self.mapper = Mapper(bus)
65 self._verbs = makelist(verbs)
66 self._rules = rules
Brad Bishopaa65f6e2015-10-27 16:28:51 -040067
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050068 def _setup(self, **kw):
69 request.route_data = {}
70 if request.method in self._verbs:
71 return self.setup(**kw)
72 else:
73 self.find(**kw)
74 raise HTTPError(405, "Method not allowed.",
75 Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040076
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050077 def __call__(self, **kw):
78 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040079
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050080 def install(self):
81 self.app.route(self._rules, callback = self,
82 method = ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -040083
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050084 @staticmethod
85 def try_mapper_call(f, callback = None, **kw):
Brad Bishopaa65f6e2015-10-27 16:28:51 -040086 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050087 return f(**kw)
88 except dbus.exceptions.DBusException, e:
89 if e.get_dbus_name() != OpenBMCMapper.MAPPER_NOT_FOUND:
90 raise
91 if callback is None:
92 def callback(e, **kw):
93 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040094
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050095 callback(e, **kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040096
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050097 @staticmethod
98 def try_properties_interface(f, *a):
Brad Bishopaa65f6e2015-10-27 16:28:51 -040099 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500100 return f(*a)
101 except dbus.exceptions.DBusException, e:
102 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
103 # interface doesn't have any properties
104 return None
105 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
106 # properties interface not implemented at all
107 return None
108 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400109
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500110class DirectoryHandler(RouteHandler):
111 verbs = 'GET'
112 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400113
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500114 def __init__(self, app, bus):
115 super(DirectoryHandler, self).__init__(
116 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400117
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500118 def find(self, path = '/'):
119 return self.try_mapper_call(
120 self.mapper.get_subtree_paths,
121 path = path, depth = 1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400122
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500123 def setup(self, path = '/'):
124 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400125
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500126 def do_get(self, path = '/'):
127 return request.route_data['map']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400128
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500129class ListNamesHandler(RouteHandler):
130 verbs = 'GET'
131 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400132
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500133 def __init__(self, app, bus):
134 super(ListNamesHandler, self).__init__(
135 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400136
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500137 def find(self, path = '/'):
138 return self.try_mapper_call(
139 self.mapper.get_subtree, path = path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400140
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500141 def setup(self, path = '/'):
142 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400143
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500144 def do_get(self, path = '/'):
145 return request.route_data['map']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400146
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500147class ListHandler(RouteHandler):
148 verbs = 'GET'
149 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500151 def __init__(self, app, bus):
152 super(ListHandler, self).__init__(
153 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400154
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500155 def find(self, path = '/'):
156 return self.try_mapper_call(
157 self.mapper.get_subtree, path = path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400158
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500159 def setup(self, path = '/'):
160 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400161
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500162 def do_get(self, path = '/'):
Brad Bishop936f5fe2015-11-03 15:10:11 -0500163 objs = {}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500164 mapper_data = request.route_data['map']
Brad Bishop936f5fe2015-11-03 15:10:11 -0500165 tree = PathTree()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400166 for x,y in mapper_data.iteritems():
Brad Bishop936f5fe2015-11-03 15:10:11 -0500167 tree[x] = y
168
169 try:
170 # Check to see if the root path implements
171 # enumerate in addition to any sub tree
172 # objects.
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500173 root = self.try_mapper_call(self.mapper.get_object,
174 path = path)
175 mapper_data[path] = root
Brad Bishop936f5fe2015-11-03 15:10:11 -0500176 except:
177 pass
178
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500179 have_enumerate = [ (x[0], self.enumerate_capable(*x)) \
180 for x in mapper_data.iteritems() \
181 if self.enumerate_capable(*x) ]
Brad Bishop936f5fe2015-11-03 15:10:11 -0500182
183 for x,y in have_enumerate:
184 objs.update(self.call_enumerate(x, y))
185 tmp = tree[x]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500186 # remove the subtree
Brad Bishop936f5fe2015-11-03 15:10:11 -0500187 del tree[x]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500188 # add the new leaf back since enumerate results don't
189 # include the object enumerate is being invoked on
Brad Bishop936f5fe2015-11-03 15:10:11 -0500190 tree[x] = tmp
191
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500192 # make dbus calls for any remaining objects
Brad Bishop936f5fe2015-11-03 15:10:11 -0500193 for x,y in tree.dataitems():
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500194 objs[x] = self.app.instance_handler.do_get(x)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400195
196 return objs
197
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500198 @staticmethod
199 def enumerate_capable(path, bus_data):
200 busses = []
201 for name, ifaces in bus_data.iteritems():
202 if OpenBMCMapper.ENUMERATE_IFACE in ifaces:
203 busses.append(name)
204 return busses
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400205
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500206 def call_enumerate(self, path, busses):
207 objs = {}
208 for b in busses:
209 obj = self.bus.get_object(b, path, introspect = False)
210 iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE)
211 objs.update(iface.enumerate())
212 return objs
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400213
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500214class MethodHandler(RouteHandler):
215 verbs = 'POST'
216 rules = '<path:path>/action/<method>'
217 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400218
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500219 def __init__(self, app, bus):
220 super(MethodHandler, self).__init__(
221 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400222
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500223 def find(self, path, method):
224 busses = self.try_mapper_call(self.mapper.get_object,
225 path = path)
226 for items in busses.iteritems():
227 m = self.find_method_on_bus(path, method, *items)
228 if m:
229 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500231 abort(404, _4034_msg %('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400232
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500233 def setup(self, path, method):
234 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400235
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500236 def do_post(self, path, method):
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400237 try:
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500238 if request.parameter_list:
239 return request.route_data['method'](*request.parameter_list)
240 else:
241 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400242
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500243 except dbus.exceptions.DBusException, e:
244 if e.get_dbus_name() == DBUS_INVALID_ARGS:
245 abort(400, str(e))
246 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400247
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500248 @staticmethod
249 def find_method_in_interface(method, obj, interface, methods):
250 if methods is None:
251 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400252
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500253 method = find_case_insensitive(method, methods.keys())
254 if method is not None:
255 iface = dbus.Interface(obj, interface)
256 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400257
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500258 def find_method_on_bus(self, path, method, bus, interfaces):
259 obj = self.bus.get_object(bus, path, introspect = False)
260 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
261 data = iface.Introspect()
262 parser = IntrospectionNodeParser(
263 ElementTree.fromstring(data),
264 intf_match = ListMatch(interfaces))
265 for x,y in parser.get_interfaces().iteritems():
266 m = self.find_method_in_interface(method, obj, x,
267 y.get('method'))
268 if m:
269 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400270
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500271class PropertyHandler(RouteHandler):
272 verbs = ['PUT', 'GET']
273 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400274
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500275 def __init__(self, app, bus):
276 super(PropertyHandler, self).__init__(
277 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400278
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500279 def find(self, path, prop):
280 self.app.instance_handler.setup(path)
281 obj = self.app.instance_handler.do_get(path)
282 try:
283 obj[prop]
284 except KeyError, e:
285 if request.method == 'PUT':
286 abort(403, _4034_msg %('property', 'created', str(e)))
287 else:
288 abort(404, _4034_msg %('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500290 return { path: obj }
291
292 def setup(self, path, prop):
293 request.route_data['obj'] = self.find(path, prop)
294
295 def do_get(self, path, prop):
296 return request.route_data['obj'][path][prop]
297
298 def do_put(self, path, prop, value = None):
299 if value is None:
300 value = request.parameter_list
301
302 prop, iface, properties_iface = self.get_host_interface(
303 path, prop, request.route_data['map'][path])
304 try:
305 properties_iface.Set(iface, prop, value)
306 except ValueError, e:
307 abort(400, str(e))
308 except dbus.exceptions.DBusException, e:
309 if e.get_dbus_name() == DBUS_INVALID_ARGS:
310 abort(403, str(e))
311 raise
312
313 def get_host_interface(self, path, prop, bus_info):
314 for bus, interfaces in bus_info.iteritems():
315 obj = self.bus.get_object(bus, path, introspect = True)
316 properties_iface = dbus.Interface(
317 obj, dbus_interface=dbus.PROPERTIES_IFACE)
318
319 info = self.get_host_interface_on_bus(
320 path, prop, properties_iface,
321 bus, interfaces)
322 if info is not None:
323 prop, iface = info
324 return prop, iface, properties_iface
325
326 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
327 for i in interfaces:
328 properties = self.try_properties_interface(iface.GetAll, i)
329 if properties is None:
330 continue
331 prop = find_case_insensitive(prop, properties.keys())
332 if prop is None:
333 continue
334 return prop, i
335
336class InstanceHandler(RouteHandler):
337 verbs = ['GET', 'PUT', 'DELETE']
338 rules = '<path:path>'
339 request_type = dict
340
341 def __init__(self, app, bus):
342 super(InstanceHandler, self).__init__(
343 app, bus, self.verbs, self.rules)
344
345 def find(self, path, callback = None):
346 return { path: self.try_mapper_call(
347 self.mapper.get_object,
348 callback,
349 path = path) }
350
351 def setup(self, path):
352 callback = None
353 if request.method == 'PUT':
354 def callback(e, **kw):
355 abort(403, _4034_msg %('resource',
356 'created', path))
357
358 if request.route_data.get('map') is None:
359 request.route_data['map'] = self.find(path, callback)
360
361 def do_get(self, path):
362 properties = {}
363 for item in request.route_data['map'][path].iteritems():
364 properties.update(self.get_properties_on_bus(
365 path, *item))
366
367 return properties
368
369 @staticmethod
370 def get_properties_on_iface(properties_iface, iface):
371 properties = InstanceHandler.try_properties_interface(
372 properties_iface.GetAll, iface)
373 if properties is None:
374 return {}
375 return properties
376
377 def get_properties_on_bus(self, path, bus, interfaces):
378 properties = {}
379 obj = self.bus.get_object(bus, path, introspect = False)
380 properties_iface = dbus.Interface(
381 obj, dbus_interface=dbus.PROPERTIES_IFACE)
382 for i in interfaces:
383 properties.update(self.get_properties_on_iface(
384 properties_iface, i))
385
386 return properties
387
388 def do_put(self, path):
389 # make sure all properties exist in the request
390 obj = set(self.do_get(path).keys())
391 req = set(request.parameter_list.keys())
392
393 diff = list(obj.difference(req))
394 if diff:
395 abort(403, _4034_msg %('resource', 'removed',
396 '%s/attr/%s' %(path, diff[0])))
397
398 diff = list(req.difference(obj))
399 if diff:
400 abort(403, _4034_msg %('resource', 'created',
401 '%s/attr/%s' %(path, diff[0])))
402
403 for p,v in request.parameter_list.iteritems():
404 self.app.property_handler.do_put(
405 path, p, v)
406
407 def do_delete(self, path):
408 for bus_info in request.route_data['map'][path].iteritems():
409 if self.bus_missing_delete(path, *bus_info):
410 abort(403, _4034_msg %('resource', 'removed',
411 path))
412
413 for bus in request.route_data['map'][path].iterkeys():
414 self.delete_on_bus(path, bus)
415
416 def bus_missing_delete(self, path, bus, interfaces):
417 return DELETE_IFACE not in interfaces
418
419 def delete_on_bus(self, path, bus):
420 obj = self.bus.get_object(bus, path, introspect = False)
421 delete_iface = dbus.Interface(
422 obj, dbus_interface = DELETE_IFACE)
423 delete_iface.Delete()
424
Brad Bishop2f428582015-12-02 10:56:11 -0500425class SessionHandler(MethodHandler):
426 ''' Handles the /login and /logout routes, manages server side session store and
427 session cookies. '''
428
429 rules = ['/login', '/logout']
430 login_str = "User '%s' logged %s"
431 bad_passwd_str = "Invalid username or password"
432 no_user_str = "No user logged in"
433 bad_json_str = "Expecting request format { 'data': [<username>, <password>] }, got '%s'"
434 _require_auth = None
435 MAX_SESSIONS = 16
436
437 def __init__(self, app, bus):
438 super(SessionHandler, self).__init__(
439 app, bus)
440 self.hmac_key = os.urandom(128)
441 self.session_store = []
442
443 @staticmethod
444 def authenticate(username, clear):
445 try:
446 encoded = spwd.getspnam(username)[1]
447 return encoded == crypt.crypt(clear, encoded)
448 except KeyError:
449 return False
450
451 def invalidate_session(self, session):
452 try:
453 self.session_store.remove(session)
454 except ValueError:
455 pass
456
457 def new_session(self):
458 sid = os.urandom(32)
459 if self.MAX_SESSIONS <= len(self.session_store):
460 self.session_store.pop()
461 self.session_store.insert(0, {'sid': sid})
462
463 return self.session_store[0]
464
465 def get_session(self, sid):
466 sids = [ x['sid'] for x in self.session_store ]
467 try:
468 return self.session_store[sids.index(sid)]
469 except ValueError:
470 return None
471
472 def get_session_from_cookie(self):
473 return self.get_session(
474 request.get_cookie('sid',
475 secret = self.hmac_key))
476
477 def do_post(self, **kw):
478 if request.path == '/login':
479 return self.do_login(**kw)
480 else:
481 return self.do_logout(**kw)
482
483 def do_logout(self, **kw):
484 session = self.get_session_from_cookie()
485 if session is not None:
486 user = session['user']
487 self.invalidate_session(session)
488 response.delete_cookie('sid')
489 return self.login_str %(user, 'out')
490
491 return self.no_user_str
492
493 def do_login(self, **kw):
494 session = self.get_session_from_cookie()
495 if session is not None:
496 return self.login_str %(session['user'], 'in')
497
498 if len(request.parameter_list) != 2:
499 abort(400, self.bad_json_str %(request.json))
500
501 if not self.authenticate(*request.parameter_list):
502 return self.bad_passwd_str
503
504 user = request.parameter_list[0]
505 session = self.new_session()
506 session['user'] = user
507 response.set_cookie('sid', session['sid'], secret = self.hmac_key,
508 secure = True,
509 httponly = True)
510 return self.login_str %(user, 'in')
511
512 def find(self, **kw):
513 pass
514
515 def setup(self, **kw):
516 pass
517
518class AuthorizationPlugin(object):
519 ''' Invokes an optional list of authorization callbacks. '''
520
521 name = 'authorization'
522 api = 2
523
524 class Compose:
525 def __init__(self, validators, callback, session_mgr):
526 self.validators = validators
527 self.callback = callback
528 self.session_mgr = session_mgr
529
530 def __call__(self, *a, **kw):
531 sid = request.get_cookie('sid', secret = self.session_mgr.hmac_key)
532 session = self.session_mgr.get_session(sid)
533 for x in self.validators:
534 x(session, *a, **kw)
535
536 return self.callback(*a, **kw)
537
538 def apply(self, callback, route):
539 undecorated = route.get_undecorated_callback()
540 if not isinstance(undecorated, RouteHandler):
541 return callback
542
543 auth_types = getattr(undecorated,
544 '_require_auth', None)
545 if not auth_types:
546 return callback
547
548 return self.Compose(auth_types, callback,
549 undecorated.app.session_handler)
550
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500551class JsonApiRequestPlugin(object):
552 ''' Ensures request content satisfies the OpenBMC json api format. '''
553 name = 'json_api_request'
554 api = 2
555
556 error_str = "Expecting request format { 'data': <value> }, got '%s'"
557 type_error_str = "Unsupported Content-Type: '%s'"
558 json_type = "application/json"
559 request_methods = ['PUT', 'POST', 'PATCH']
560
561 @staticmethod
562 def content_expected():
563 return request.method in JsonApiRequestPlugin.request_methods
564
565 def validate_request(self):
566 if request.content_length > 0 and \
567 request.content_type != self.json_type:
568 abort(415, self.type_error_str %(request.content_type))
569
570 try:
571 request.parameter_list = request.json.get('data')
572 except ValueError, e:
573 abort(400, str(e))
574 except (AttributeError, KeyError, TypeError):
575 abort(400, self.error_str %(request.json))
576
577 def apply(self, callback, route):
578 verbs = getattr(route.get_undecorated_callback(),
579 '_verbs', None)
580 if verbs is None:
581 return callback
582
583 if not set(self.request_methods).intersection(verbs):
584 return callback
585
586 def wrap(*a, **kw):
587 if self.content_expected():
588 self.validate_request()
589 return callback(*a, **kw)
590
591 return wrap
592
593class JsonApiRequestTypePlugin(object):
594 ''' Ensures request content type satisfies the OpenBMC json api format. '''
595 name = 'json_api_method_request'
596 api = 2
597
598 error_str = "Expecting request format { 'data': %s }, got '%s'"
599
600 def apply(self, callback, route):
601 request_type = getattr(route.get_undecorated_callback(),
602 'request_type', None)
603 if request_type is None:
604 return callback
605
606 def validate_request():
607 if not isinstance(request.parameter_list, request_type):
608 abort(400, self.error_str %(str(request_type), request.json))
609
610 def wrap(*a, **kw):
611 if JsonApiRequestPlugin.content_expected():
612 validate_request()
613 return callback(*a, **kw)
614
615 return wrap
616
617class JsonApiResponsePlugin(object):
618 ''' Emits normal responses in the OpenBMC json api format. '''
619 name = 'json_api_response'
620 api = 2
621
622 def apply(self, callback, route):
623 def wrap(*a, **kw):
624 resp = { 'data': callback(*a, **kw) }
625 resp['status'] = 'ok'
626 resp['message'] = response.status_line
627 return resp
628 return wrap
629
630class JsonApiErrorsPlugin(object):
631 ''' Emits error responses in the OpenBMC json api format. '''
632 name = 'json_api_errors'
633 api = 2
634
635 def __init__(self, **kw):
636 self.app = None
637 self.function_type = None
638 self.original = None
639 self.json_opts = { x:y for x,y in kw.iteritems() \
640 if x in ['indent','sort_keys'] }
641
642 def setup(self, app):
643 self.app = app
644 self.function_type = type(app.default_error_handler)
645 self.original = app.default_error_handler
646 self.app.default_error_handler = self.function_type(
647 self.json_errors, app, Bottle)
648
649 def apply(self, callback, route):
650 return callback
651
652 def close(self):
653 self.app.default_error_handler = self.function_type(
654 self.original, self.app, Bottle)
655
656 def json_errors(self, res, error):
657 response_object = {'status': 'error', 'data': {} }
658 response_object['message'] = error.status_line
659 response_object['data']['description'] = str(error.body)
660 if error.status_code == 500:
661 response_object['data']['exception'] = repr(error.exception)
662 response_object['data']['traceback'] = error.traceback.splitlines()
663
664 json_response = json.dumps(response_object, **self.json_opts)
Brad Bishop9bfeec22015-11-17 09:14:50 -0500665 response.content_type = 'application/json'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500666 return json_response
667
668class RestApp(Bottle):
669 def __init__(self, bus):
670 super(RestApp, self).__init__(autojson = False)
Brad Bishop53fd4932015-10-30 09:22:30 -0400671 self.bus = bus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500672 self.mapper = Mapper(bus)
673
674 self.install_hooks()
675 self.install_plugins()
676 self.create_handlers()
677 self.install_handlers()
678
679 def install_plugins(self):
680 # install json api plugins
681 json_kw = {'indent': 2, 'sort_keys': True}
682 self.install(JSONPlugin(**json_kw))
683 self.install(JsonApiErrorsPlugin(**json_kw))
Brad Bishop2f428582015-12-02 10:56:11 -0500684 self.install(AuthorizationPlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500685 self.install(JsonApiResponsePlugin())
686 self.install(JsonApiRequestPlugin())
687 self.install(JsonApiRequestTypePlugin())
688
689 def install_hooks(self):
690 self.real_router_match = self.router.match
691 self.router.match = self.custom_router_match
692 self.add_hook('before_request', self.strip_extra_slashes)
693
694 def create_handlers(self):
695 # create route handlers
Brad Bishop2f428582015-12-02 10:56:11 -0500696 self.session_handler = SessionHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500697 self.directory_handler = DirectoryHandler(self, self.bus)
698 self.list_names_handler = ListNamesHandler(self, self.bus)
699 self.list_handler = ListHandler(self, self.bus)
700 self.method_handler = MethodHandler(self, self.bus)
701 self.property_handler = PropertyHandler(self, self.bus)
702 self.instance_handler = InstanceHandler(self, self.bus)
703
704 def install_handlers(self):
Brad Bishop2f428582015-12-02 10:56:11 -0500705 self.session_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500706 self.directory_handler.install()
707 self.list_names_handler.install()
708 self.list_handler.install()
709 self.method_handler.install()
710 self.property_handler.install()
711 # this has to come last, since it matches everything
712 self.instance_handler.install()
713
714 def custom_router_match(self, environ):
715 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
716 needed doesn't work for us since the instance rules match everything.
717 This monkey-patch lets the route handler figure out which response is
718 needed. This could be accomplished with a hook but that would require
719 calling the router match function twice.
720 '''
721 route, args = self.real_router_match(environ)
722 if isinstance(route.callback, RouteHandler):
723 route.callback._setup(**args)
724
725 return route, args
726
727 @staticmethod
728 def strip_extra_slashes():
729 path = request.environ['PATH_INFO']
730 trailing = ("","/")[path[-1] == '/']
731 parts = filter(bool, path.split('/'))
732 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400733
734if __name__ == '__main__':
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500735 log = logging.getLogger('Rocket.Errors')
736 log.setLevel(logging.INFO)
737 log.addHandler(logging.StreamHandler(sys.stdout))
738
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400739 bus = dbus.SystemBus()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500740 app = RestApp(bus)
741 default_cert = os.path.join(sys.prefix, 'share',
742 os.path.basename(__file__), 'cert.pem')
743
744 server = Rocket(('0.0.0.0',
745 443,
746 default_cert,
747 default_cert),
748 'wsgi', {'wsgi_app': app})
749 server.start()