blob: 47dacaaf34585c2bdd50e9a5c5dc03c212b7f814 [file] [log] [blame]
Brad Bishop68caa1e2016-03-04 15:42:08 -05001# 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
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050017import os
Brad Bishopaa65f6e2015-10-27 16:28:51 -040018import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import dbus.exceptions
20import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050021from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopb103d2d2016-03-04 16:19:14 -050023import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050024from obmc.dbuslib.introspection import IntrospectionNodeParser
25import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050026import spwd
27import grp
28import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040029
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050030DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040031DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050032DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
33DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050034DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050035DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050036
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050037_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040038
Brad Bishop87b63c12016-03-18 14:47:51 -040039
Brad Bishop2f428582015-12-02 10:56:11 -050040def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040041 ''' Authorization plugin callback that checks
42 that the user is logged in. '''
43 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040044 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040045
Brad Bishop2f428582015-12-02 10:56:11 -050046
47class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -040048 ''' Authorization plugin callback that checks that the user is logged in
49 and a member of a group. '''
50 def __init__(self, group):
51 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -050052
Brad Bishop87b63c12016-03-18 14:47:51 -040053 def __call__(self, session, *a, **kw):
54 valid_user(session, *a, **kw)
55 res = False
Brad Bishop2f428582015-12-02 10:56:11 -050056
Brad Bishop87b63c12016-03-18 14:47:51 -040057 try:
58 res = session['user'] in grp.getgrnam(self.group)[3]
59 except KeyError:
60 pass
Brad Bishop2f428582015-12-02 10:56:11 -050061
Brad Bishop87b63c12016-03-18 14:47:51 -040062 if not res:
63 abort(403, 'Insufficient access')
64
Brad Bishop2f428582015-12-02 10:56:11 -050065
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050066class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -040067 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -050068 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -040069
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -050070 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -040071 self.app = app
72 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -050073 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -040074 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -040075 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -050076 self._content_type = content_type
Brad Bishop0f79e522016-03-18 13:33:17 -040077 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040078
Brad Bishop88c76a42017-02-21 00:02:02 -050079 if 'GET' in self._verbs:
80 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -050081 if 'OPTIONS' not in self._verbs:
82 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -050083
Brad Bishop87b63c12016-03-18 14:47:51 -040084 def _setup(self, **kw):
85 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -050086
Brad Bishop87b63c12016-03-18 14:47:51 -040087 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -050088 if request.method != 'OPTIONS':
89 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -050090
Brad Bishopd4c1c552017-02-21 00:07:28 -050091 # Javascript implementations will not send credentials
92 # with an OPTIONS request. Don't help malicious clients
93 # by checking the path here and returning a 404 if the
94 # path doesn't exist.
95 return None
Brad Bishop88c76a42017-02-21 00:02:02 -050096
Brad Bishopd4c1c552017-02-21 00:07:28 -050097 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -050098 raise HTTPError(
99 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400100
Brad Bishop87b63c12016-03-18 14:47:51 -0400101 def __call__(self, **kw):
102 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400103
Brad Bishop88c76a42017-02-21 00:02:02 -0500104 def do_head(self, **kw):
105 return self.do_get(**kw)
106
Brad Bishopd4c1c552017-02-21 00:07:28 -0500107 def do_options(self, **kw):
108 for v in self._verbs:
109 response.set_header(
110 'Allow',
111 ','.join(self._verbs))
112 return None
113
Brad Bishop87b63c12016-03-18 14:47:51 -0400114 def install(self):
115 self.app.route(
116 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500117 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400118
Brad Bishop87b63c12016-03-18 14:47:51 -0400119 @staticmethod
120 def try_mapper_call(f, callback=None, **kw):
121 try:
122 return f(**kw)
123 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500124 if e.get_dbus_name() == \
125 'org.freedesktop.DBus.Error.ObjectPathInUse':
126 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500127 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400128 raise
129 if callback is None:
130 def callback(e, **kw):
131 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400132
Brad Bishop87b63c12016-03-18 14:47:51 -0400133 callback(e, **kw)
134
135 @staticmethod
136 def try_properties_interface(f, *a):
137 try:
138 return f(*a)
139 except dbus.exceptions.DBusException, e:
140 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
141 # interface doesn't have any properties
142 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400143 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
144 # interface doesn't have any properties
145 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400146 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
147 # properties interface not implemented at all
148 return None
149 raise
150
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400151
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500152class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400153 verbs = 'GET'
154 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400155
Brad Bishop87b63c12016-03-18 14:47:51 -0400156 def __init__(self, app, bus):
157 super(DirectoryHandler, self).__init__(
158 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400159
Brad Bishop87b63c12016-03-18 14:47:51 -0400160 def find(self, path='/'):
161 return self.try_mapper_call(
162 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400163
Brad Bishop87b63c12016-03-18 14:47:51 -0400164 def setup(self, path='/'):
165 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400166
Brad Bishop87b63c12016-03-18 14:47:51 -0400167 def do_get(self, path='/'):
168 return request.route_data['map']
169
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400170
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500171class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400172 verbs = 'GET'
173 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400174
Brad Bishop87b63c12016-03-18 14:47:51 -0400175 def __init__(self, app, bus):
176 super(ListNamesHandler, self).__init__(
177 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400178
Brad Bishop87b63c12016-03-18 14:47:51 -0400179 def find(self, path='/'):
180 return self.try_mapper_call(
181 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400182
Brad Bishop87b63c12016-03-18 14:47:51 -0400183 def setup(self, path='/'):
184 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400185
Brad Bishop87b63c12016-03-18 14:47:51 -0400186 def do_get(self, path='/'):
187 return request.route_data['map']
188
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400189
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500190class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400191 verbs = 'GET'
192 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400193
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 def __init__(self, app, bus):
195 super(ListHandler, self).__init__(
196 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400197
Brad Bishop87b63c12016-03-18 14:47:51 -0400198 def find(self, path='/'):
199 return self.try_mapper_call(
200 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400201
Brad Bishop87b63c12016-03-18 14:47:51 -0400202 def setup(self, path='/'):
203 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400204
Brad Bishop87b63c12016-03-18 14:47:51 -0400205 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400206 return {x: y for x, y in self.mapper.enumerate_subtree(
207 path,
208 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400209
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400210
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500211class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400212 verbs = 'POST'
213 rules = '<path:path>/action/<method>'
214 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500215 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400216
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 def __init__(self, app, bus):
218 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500219 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400220
Brad Bishop87b63c12016-03-18 14:47:51 -0400221 def find(self, path, method):
222 busses = self.try_mapper_call(
223 self.mapper.get_object, path=path)
224 for items in busses.iteritems():
225 m = self.find_method_on_bus(path, method, *items)
226 if m:
227 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400228
Brad Bishop87b63c12016-03-18 14:47:51 -0400229 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 def setup(self, path, method):
232 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400233
Brad Bishop87b63c12016-03-18 14:47:51 -0400234 def do_post(self, path, method):
235 try:
236 if request.parameter_list:
237 return request.route_data['method'](*request.parameter_list)
238 else:
239 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400240
Brad Bishop87b63c12016-03-18 14:47:51 -0400241 except dbus.exceptions.DBusException, e:
242 if e.get_dbus_name() == DBUS_INVALID_ARGS:
243 abort(400, str(e))
244 if e.get_dbus_name() == DBUS_TYPE_ERROR:
245 abort(400, str(e))
246 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400247
Brad Bishop87b63c12016-03-18 14:47:51 -0400248 @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 Bishop6d190602016-04-15 13:09:39 -0400253 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400254 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 Bishop87b63c12016-03-18 14:47:51 -0400258 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),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500264 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400265 for x, y in parser.get_interfaces().iteritems():
266 m = self.find_method_in_interface(
267 method, obj, x, y.get('method'))
268 if m:
269 return m
270
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400271
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500272class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 verbs = ['PUT', 'GET']
274 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500275 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400276
Brad Bishop87b63c12016-03-18 14:47:51 -0400277 def __init__(self, app, bus):
278 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500279 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400280
Brad Bishop87b63c12016-03-18 14:47:51 -0400281 def find(self, path, prop):
282 self.app.instance_handler.setup(path)
283 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500284 real_name = obmc.utils.misc.find_case_insensitive(
285 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400286
Brad Bishop56ad87f2017-02-21 23:33:29 -0500287 if not real_name:
288 if request.method == 'PUT':
289 abort(403, _4034_msg % ('property', 'created', prop))
290 else:
291 abort(404, _4034_msg % ('property', 'found', prop))
292 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500293
Brad Bishop87b63c12016-03-18 14:47:51 -0400294 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500295 name, obj = self.find(path, prop)
296 request.route_data['obj'] = obj
297 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500298
Brad Bishop87b63c12016-03-18 14:47:51 -0400299 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500300 name = request.route_data['name']
301 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def do_put(self, path, prop, value=None):
304 if value is None:
305 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500306
Brad Bishop87b63c12016-03-18 14:47:51 -0400307 prop, iface, properties_iface = self.get_host_interface(
308 path, prop, request.route_data['map'][path])
309 try:
310 properties_iface.Set(iface, prop, value)
311 except ValueError, e:
312 abort(400, str(e))
313 except dbus.exceptions.DBusException, e:
314 if e.get_dbus_name() == DBUS_INVALID_ARGS:
315 abort(403, str(e))
316 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500317
Brad Bishop87b63c12016-03-18 14:47:51 -0400318 def get_host_interface(self, path, prop, bus_info):
319 for bus, interfaces in bus_info.iteritems():
320 obj = self.bus.get_object(bus, path, introspect=True)
321 properties_iface = dbus.Interface(
322 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500323
Brad Bishop87b63c12016-03-18 14:47:51 -0400324 info = self.get_host_interface_on_bus(
325 path, prop, properties_iface, bus, interfaces)
326 if info is not None:
327 prop, iface = info
328 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500329
Brad Bishop87b63c12016-03-18 14:47:51 -0400330 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
331 for i in interfaces:
332 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500333 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400334 continue
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500335 prop = obmc.utils.misc.find_case_insensitive(
336 prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400337 if prop is None:
338 continue
339 return prop, i
340
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500341
Brad Bishop2503bd62015-12-16 17:56:12 -0500342class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 verbs = ['GET']
344 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500345
Brad Bishop87b63c12016-03-18 14:47:51 -0400346 def __init__(self, app, bus):
347 super(SchemaHandler, self).__init__(
348 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500349
Brad Bishop87b63c12016-03-18 14:47:51 -0400350 def find(self, path):
351 return self.try_mapper_call(
352 self.mapper.get_object,
353 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500354
Brad Bishop87b63c12016-03-18 14:47:51 -0400355 def setup(self, path):
356 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500357
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 def do_get(self, path):
359 schema = {}
360 for x in request.route_data['map'].iterkeys():
361 obj = self.bus.get_object(x, path, introspect=False)
362 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
363 data = iface.Introspect()
364 parser = IntrospectionNodeParser(
365 ElementTree.fromstring(data))
366 for x, y in parser.get_interfaces().iteritems():
367 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500368
Brad Bishop87b63c12016-03-18 14:47:51 -0400369 return schema
370
Brad Bishop2503bd62015-12-16 17:56:12 -0500371
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500372class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400373 verbs = ['GET', 'PUT', 'DELETE']
374 rules = '<path:path>'
375 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500376
Brad Bishop87b63c12016-03-18 14:47:51 -0400377 def __init__(self, app, bus):
378 super(InstanceHandler, self).__init__(
379 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500380
Brad Bishop87b63c12016-03-18 14:47:51 -0400381 def find(self, path, callback=None):
382 return {path: self.try_mapper_call(
383 self.mapper.get_object,
384 callback,
385 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500386
Brad Bishop87b63c12016-03-18 14:47:51 -0400387 def setup(self, path):
388 callback = None
389 if request.method == 'PUT':
390 def callback(e, **kw):
391 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500392
Brad Bishop87b63c12016-03-18 14:47:51 -0400393 if request.route_data.get('map') is None:
394 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500395
Brad Bishop87b63c12016-03-18 14:47:51 -0400396 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400397 return self.mapper.enumerate_object(
398 path,
399 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500400
Brad Bishop87b63c12016-03-18 14:47:51 -0400401 def do_put(self, path):
402 # make sure all properties exist in the request
403 obj = set(self.do_get(path).keys())
404 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500405
Brad Bishop87b63c12016-03-18 14:47:51 -0400406 diff = list(obj.difference(req))
407 if diff:
408 abort(403, _4034_msg % (
409 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500410
Brad Bishop87b63c12016-03-18 14:47:51 -0400411 diff = list(req.difference(obj))
412 if diff:
413 abort(403, _4034_msg % (
414 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500415
Brad Bishop87b63c12016-03-18 14:47:51 -0400416 for p, v in request.parameter_list.iteritems():
417 self.app.property_handler.do_put(
418 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500419
Brad Bishop87b63c12016-03-18 14:47:51 -0400420 def do_delete(self, path):
421 for bus_info in request.route_data['map'][path].iteritems():
422 if self.bus_missing_delete(path, *bus_info):
423 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500424
Brad Bishop87b63c12016-03-18 14:47:51 -0400425 for bus in request.route_data['map'][path].iterkeys():
426 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500427
Brad Bishop87b63c12016-03-18 14:47:51 -0400428 def bus_missing_delete(self, path, bus, interfaces):
429 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500430
Brad Bishop87b63c12016-03-18 14:47:51 -0400431 def delete_on_bus(self, path, bus):
432 obj = self.bus.get_object(bus, path, introspect=False)
433 delete_iface = dbus.Interface(
434 obj, dbus_interface=DELETE_IFACE)
435 delete_iface.Delete()
436
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500437
Brad Bishop2f428582015-12-02 10:56:11 -0500438class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 ''' Handles the /login and /logout routes, manages
440 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500441
Brad Bishop87b63c12016-03-18 14:47:51 -0400442 rules = ['/login', '/logout']
443 login_str = "User '%s' logged %s"
444 bad_passwd_str = "Invalid username or password"
445 no_user_str = "No user logged in"
446 bad_json_str = "Expecting request format { 'data': " \
447 "[<username>, <password>] }, got '%s'"
448 _require_auth = None
449 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500450
Brad Bishop87b63c12016-03-18 14:47:51 -0400451 def __init__(self, app, bus):
452 super(SessionHandler, self).__init__(
453 app, bus)
454 self.hmac_key = os.urandom(128)
455 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500456
Brad Bishop87b63c12016-03-18 14:47:51 -0400457 @staticmethod
458 def authenticate(username, clear):
459 try:
460 encoded = spwd.getspnam(username)[1]
461 return encoded == crypt.crypt(clear, encoded)
462 except KeyError:
463 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def invalidate_session(self, session):
466 try:
467 self.session_store.remove(session)
468 except ValueError:
469 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500470
Brad Bishop87b63c12016-03-18 14:47:51 -0400471 def new_session(self):
472 sid = os.urandom(32)
473 if self.MAX_SESSIONS <= len(self.session_store):
474 self.session_store.pop()
475 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500476
Brad Bishop87b63c12016-03-18 14:47:51 -0400477 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500478
Brad Bishop87b63c12016-03-18 14:47:51 -0400479 def get_session(self, sid):
480 sids = [x['sid'] for x in self.session_store]
481 try:
482 return self.session_store[sids.index(sid)]
483 except ValueError:
484 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500485
Brad Bishop87b63c12016-03-18 14:47:51 -0400486 def get_session_from_cookie(self):
487 return self.get_session(
488 request.get_cookie(
489 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500490
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 def do_post(self, **kw):
492 if request.path == '/login':
493 return self.do_login(**kw)
494 else:
495 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 def do_logout(self, **kw):
498 session = self.get_session_from_cookie()
499 if session is not None:
500 user = session['user']
501 self.invalidate_session(session)
502 response.delete_cookie('sid')
503 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500504
Brad Bishop87b63c12016-03-18 14:47:51 -0400505 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500506
Brad Bishop87b63c12016-03-18 14:47:51 -0400507 def do_login(self, **kw):
508 session = self.get_session_from_cookie()
509 if session is not None:
510 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 if len(request.parameter_list) != 2:
513 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500514
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400516 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500517
Brad Bishop87b63c12016-03-18 14:47:51 -0400518 user = request.parameter_list[0]
519 session = self.new_session()
520 session['user'] = user
521 response.set_cookie(
522 'sid', session['sid'], secret=self.hmac_key,
523 secure=True,
524 httponly=True)
525 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500526
Brad Bishop87b63c12016-03-18 14:47:51 -0400527 def find(self, **kw):
528 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500529
Brad Bishop87b63c12016-03-18 14:47:51 -0400530 def setup(self, **kw):
531 pass
532
Brad Bishop2f428582015-12-02 10:56:11 -0500533
534class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400535 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500536
Brad Bishop87b63c12016-03-18 14:47:51 -0400537 name = 'authorization'
538 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500539
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 class Compose:
541 def __init__(self, validators, callback, session_mgr):
542 self.validators = validators
543 self.callback = callback
544 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500545
Brad Bishop87b63c12016-03-18 14:47:51 -0400546 def __call__(self, *a, **kw):
547 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
548 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500549 if request.method != 'OPTIONS':
550 for x in self.validators:
551 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500552
Brad Bishop87b63c12016-03-18 14:47:51 -0400553 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500554
Brad Bishop87b63c12016-03-18 14:47:51 -0400555 def apply(self, callback, route):
556 undecorated = route.get_undecorated_callback()
557 if not isinstance(undecorated, RouteHandler):
558 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500559
Brad Bishop87b63c12016-03-18 14:47:51 -0400560 auth_types = getattr(
561 undecorated, '_require_auth', None)
562 if not auth_types:
563 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500564
Brad Bishop87b63c12016-03-18 14:47:51 -0400565 return self.Compose(
566 auth_types, callback, undecorated.app.session_handler)
567
Brad Bishop2f428582015-12-02 10:56:11 -0500568
Brad Bishopd0c404a2017-02-21 09:23:25 -0500569class CorsPlugin(object):
570 ''' Add CORS headers. '''
571
572 name = 'cors'
573 api = 2
574
575 @staticmethod
576 def process_origin():
577 origin = request.headers.get('Origin')
578 if origin:
579 response.add_header('Access-Control-Allow-Origin', origin)
580 response.add_header(
581 'Access-Control-Allow-Credentials', 'true')
582
583 @staticmethod
584 def process_method_and_headers(verbs):
585 method = request.headers.get('Access-Control-Request-Method')
586 headers = request.headers.get('Access-Control-Request-Headers')
587 if headers:
588 headers = [x.lower() for x in headers.split(',')]
589
590 if method in verbs \
591 and headers == ['content-type']:
592 response.add_header('Access-Control-Allow-Methods', method)
593 response.add_header(
594 'Access-Control-Allow-Headers', 'Content-Type')
595
596 def __init__(self, app):
597 app.install_error_callback(self.error_callback)
598
599 def apply(self, callback, route):
600 undecorated = route.get_undecorated_callback()
601 if not isinstance(undecorated, RouteHandler):
602 return callback
603
604 if not getattr(undecorated, '_enable_cors', None):
605 return callback
606
607 def wrap(*a, **kw):
608 self.process_origin()
609 self.process_method_and_headers(undecorated._verbs)
610 return callback(*a, **kw)
611
612 return wrap
613
614 def error_callback(self, **kw):
615 self.process_origin()
616
617
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500618class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400619 ''' Ensures request content satisfies the OpenBMC json api format. '''
620 name = 'json_api_request'
621 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500622
Brad Bishop87b63c12016-03-18 14:47:51 -0400623 error_str = "Expecting request format { 'data': <value> }, got '%s'"
624 type_error_str = "Unsupported Content-Type: '%s'"
625 json_type = "application/json"
626 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500627
Brad Bishop87b63c12016-03-18 14:47:51 -0400628 @staticmethod
629 def content_expected():
630 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500631
Brad Bishop87b63c12016-03-18 14:47:51 -0400632 def validate_request(self):
633 if request.content_length > 0 and \
634 request.content_type != self.json_type:
635 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500636
Brad Bishop87b63c12016-03-18 14:47:51 -0400637 try:
638 request.parameter_list = request.json.get('data')
639 except ValueError, e:
640 abort(400, str(e))
641 except (AttributeError, KeyError, TypeError):
642 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500643
Brad Bishop87b63c12016-03-18 14:47:51 -0400644 def apply(self, callback, route):
645 verbs = getattr(
646 route.get_undecorated_callback(), '_verbs', None)
647 if verbs is None:
648 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500649
Brad Bishop87b63c12016-03-18 14:47:51 -0400650 if not set(self.request_methods).intersection(verbs):
651 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500652
Brad Bishop87b63c12016-03-18 14:47:51 -0400653 def wrap(*a, **kw):
654 if self.content_expected():
655 self.validate_request()
656 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500657
Brad Bishop87b63c12016-03-18 14:47:51 -0400658 return wrap
659
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500660
661class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400662 ''' Ensures request content type satisfies the OpenBMC json api format. '''
663 name = 'json_api_method_request'
664 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500665
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500667
Brad Bishop87b63c12016-03-18 14:47:51 -0400668 def apply(self, callback, route):
669 request_type = getattr(
670 route.get_undecorated_callback(), 'request_type', None)
671 if request_type is None:
672 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500673
Brad Bishop87b63c12016-03-18 14:47:51 -0400674 def validate_request():
675 if not isinstance(request.parameter_list, request_type):
676 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500677
Brad Bishop87b63c12016-03-18 14:47:51 -0400678 def wrap(*a, **kw):
679 if JsonApiRequestPlugin.content_expected():
680 validate_request()
681 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500682
Brad Bishop87b63c12016-03-18 14:47:51 -0400683 return wrap
684
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500685
Brad Bishop080a48e2017-02-21 22:34:43 -0500686class JsonErrorsPlugin(JSONPlugin):
687 ''' Extend the Bottle JSONPlugin such that it also encodes error
688 responses. '''
689
690 def __init__(self, app, **kw):
691 super(JsonErrorsPlugin, self).__init__(**kw)
692 self.json_opts = {
693 x: y for x, y in kw.iteritems()
694 if x in ['indent', 'sort_keys']}
695 app.install_error_callback(self.error_callback)
696
697 def error_callback(self, response_object, response_body, **kw):
698 response_body['body'] = json.dumps(response_object, **self.json_opts)
699 response.content_type = 'application/json'
700
701
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500702class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500703 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400704 name = 'json_api_response'
705 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500706
Brad Bishopd4c1c552017-02-21 00:07:28 -0500707 @staticmethod
708 def has_body():
709 return request.method not in ['OPTIONS']
710
Brad Bishop080a48e2017-02-21 22:34:43 -0500711 def __init__(self, app):
712 app.install_error_callback(self.error_callback)
713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 def apply(self, callback, route):
715 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500716 data = callback(*a, **kw)
717 if self.has_body():
718 resp = {'data': data}
719 resp['status'] = 'ok'
720 resp['message'] = response.status_line
721 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400722 return wrap
723
Brad Bishop080a48e2017-02-21 22:34:43 -0500724 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400725 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -0500726 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -0500727 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400728 if error.status_code == 500:
729 response_object['data']['exception'] = repr(error.exception)
730 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500731
Brad Bishop87b63c12016-03-18 14:47:51 -0400732
Brad Bishop080a48e2017-02-21 22:34:43 -0500733class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400734 ''' Json javascript wrapper. '''
735 name = 'jsonp'
736 api = 2
737
Brad Bishop080a48e2017-02-21 22:34:43 -0500738 def __init__(self, app, **kw):
739 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400740
741 @staticmethod
742 def to_jsonp(json):
743 jwrapper = request.query.callback or None
744 if(jwrapper):
745 response.set_header('Content-Type', 'application/javascript')
746 json = jwrapper + '(' + json + ');'
747 return json
748
749 def apply(self, callback, route):
750 def wrap(*a, **kw):
751 return self.to_jsonp(callback(*a, **kw))
752 return wrap
753
Brad Bishop080a48e2017-02-21 22:34:43 -0500754 def error_callback(self, response_body, **kw):
755 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400756
757
Brad Bishop2c6fc762016-08-29 15:53:25 -0400758class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400759 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400760 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400761 self.bus = dbus.SystemBus()
762 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -0500763 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500764
Brad Bishop87b63c12016-03-18 14:47:51 -0400765 self.install_hooks()
766 self.install_plugins()
767 self.create_handlers()
768 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500769
Brad Bishop87b63c12016-03-18 14:47:51 -0400770 def install_plugins(self):
771 # install json api plugins
772 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400773 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -0500774 self.install(CorsPlugin(self))
Brad Bishop080a48e2017-02-21 22:34:43 -0500775 self.install(JsonpPlugin(self, **json_kw))
776 self.install(JsonErrorsPlugin(self, **json_kw))
777 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -0400778 self.install(JsonApiRequestPlugin())
779 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500780
Brad Bishop87b63c12016-03-18 14:47:51 -0400781 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -0500782 self.error_handler_type = type(self.default_error_handler)
783 self.original_error_handler = self.default_error_handler
784 self.default_error_handler = self.error_handler_type(
785 self.custom_error_handler, self, Bottle)
786
Brad Bishop87b63c12016-03-18 14:47:51 -0400787 self.real_router_match = self.router.match
788 self.router.match = self.custom_router_match
789 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500790
Brad Bishop87b63c12016-03-18 14:47:51 -0400791 def create_handlers(self):
792 # create route handlers
793 self.session_handler = SessionHandler(self, self.bus)
794 self.directory_handler = DirectoryHandler(self, self.bus)
795 self.list_names_handler = ListNamesHandler(self, self.bus)
796 self.list_handler = ListHandler(self, self.bus)
797 self.method_handler = MethodHandler(self, self.bus)
798 self.property_handler = PropertyHandler(self, self.bus)
799 self.schema_handler = SchemaHandler(self, self.bus)
800 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500801
Brad Bishop87b63c12016-03-18 14:47:51 -0400802 def install_handlers(self):
803 self.session_handler.install()
804 self.directory_handler.install()
805 self.list_names_handler.install()
806 self.list_handler.install()
807 self.method_handler.install()
808 self.property_handler.install()
809 self.schema_handler.install()
810 # this has to come last, since it matches everything
811 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500812
Brad Bishop080a48e2017-02-21 22:34:43 -0500813 def install_error_callback(self, callback):
814 self.error_callbacks.insert(0, callback)
815
Brad Bishop87b63c12016-03-18 14:47:51 -0400816 def custom_router_match(self, environ):
817 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
818 needed doesn't work for us since the instance rules match
819 everything. This monkey-patch lets the route handler figure
820 out which response is needed. This could be accomplished
821 with a hook but that would require calling the router match
822 function twice.
823 '''
824 route, args = self.real_router_match(environ)
825 if isinstance(route.callback, RouteHandler):
826 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500827
Brad Bishop87b63c12016-03-18 14:47:51 -0400828 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500829
Brad Bishop080a48e2017-02-21 22:34:43 -0500830 def custom_error_handler(self, res, error):
831 ''' Allow plugins to modify error reponses too via this custom
832 error handler. '''
833
834 response_object = {}
835 response_body = {}
836 for x in self.error_callbacks:
837 x(error=error,
838 response_object=response_object,
839 response_body=response_body)
840
841 return response_body.get('body', "")
842
Brad Bishop87b63c12016-03-18 14:47:51 -0400843 @staticmethod
844 def strip_extra_slashes():
845 path = request.environ['PATH_INFO']
846 trailing = ("", "/")[path[-1] == '/']
847 parts = filter(bool, path.split('/'))
848 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing