blob: e19d9763a1689cb89ba9eb31f19d0171ed75796c [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
Brad Bishop87b63c12016-03-18 14:47:51 -040070 def __init__(self, app, bus, verbs, rules):
71 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
Brad Bishop0f79e522016-03-18 13:33:17 -040076 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040077
Brad Bishop88c76a42017-02-21 00:02:02 -050078 if 'GET' in self._verbs:
79 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -050080 if 'OPTIONS' not in self._verbs:
81 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -050082
Brad Bishop87b63c12016-03-18 14:47:51 -040083 def _setup(self, **kw):
84 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -050085
Brad Bishop87b63c12016-03-18 14:47:51 -040086 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -050087 if request.method != 'OPTIONS':
88 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -050089
Brad Bishopd4c1c552017-02-21 00:07:28 -050090 # Javascript implementations will not send credentials
91 # with an OPTIONS request. Don't help malicious clients
92 # by checking the path here and returning a 404 if the
93 # path doesn't exist.
94 return None
Brad Bishop88c76a42017-02-21 00:02:02 -050095
Brad Bishopd4c1c552017-02-21 00:07:28 -050096 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -050097 raise HTTPError(
98 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040099
Brad Bishop87b63c12016-03-18 14:47:51 -0400100 def __call__(self, **kw):
101 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400102
Brad Bishop88c76a42017-02-21 00:02:02 -0500103 def do_head(self, **kw):
104 return self.do_get(**kw)
105
Brad Bishopd4c1c552017-02-21 00:07:28 -0500106 def do_options(self, **kw):
107 for v in self._verbs:
108 response.set_header(
109 'Allow',
110 ','.join(self._verbs))
111 return None
112
Brad Bishop87b63c12016-03-18 14:47:51 -0400113 def install(self):
114 self.app.route(
115 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500116 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400117
Brad Bishop87b63c12016-03-18 14:47:51 -0400118 @staticmethod
119 def try_mapper_call(f, callback=None, **kw):
120 try:
121 return f(**kw)
122 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500123 if e.get_dbus_name() == \
124 'org.freedesktop.DBus.Error.ObjectPathInUse':
125 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500126 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400127 raise
128 if callback is None:
129 def callback(e, **kw):
130 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400131
Brad Bishop87b63c12016-03-18 14:47:51 -0400132 callback(e, **kw)
133
134 @staticmethod
135 def try_properties_interface(f, *a):
136 try:
137 return f(*a)
138 except dbus.exceptions.DBusException, e:
139 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
140 # interface doesn't have any properties
141 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400142 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
143 # interface doesn't have any properties
144 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400145 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
146 # properties interface not implemented at all
147 return None
148 raise
149
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500151class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400152 verbs = 'GET'
153 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400154
Brad Bishop87b63c12016-03-18 14:47:51 -0400155 def __init__(self, app, bus):
156 super(DirectoryHandler, self).__init__(
157 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400158
Brad Bishop87b63c12016-03-18 14:47:51 -0400159 def find(self, path='/'):
160 return self.try_mapper_call(
161 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400162
Brad Bishop87b63c12016-03-18 14:47:51 -0400163 def setup(self, path='/'):
164 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400165
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 def do_get(self, path='/'):
167 return request.route_data['map']
168
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400169
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500170class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400171 verbs = 'GET'
172 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400173
Brad Bishop87b63c12016-03-18 14:47:51 -0400174 def __init__(self, app, bus):
175 super(ListNamesHandler, self).__init__(
176 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400177
Brad Bishop87b63c12016-03-18 14:47:51 -0400178 def find(self, path='/'):
179 return self.try_mapper_call(
180 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400181
Brad Bishop87b63c12016-03-18 14:47:51 -0400182 def setup(self, path='/'):
183 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400184
Brad Bishop87b63c12016-03-18 14:47:51 -0400185 def do_get(self, path='/'):
186 return request.route_data['map']
187
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400188
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500189class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400190 verbs = 'GET'
191 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400192
Brad Bishop87b63c12016-03-18 14:47:51 -0400193 def __init__(self, app, bus):
194 super(ListHandler, self).__init__(
195 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400196
Brad Bishop87b63c12016-03-18 14:47:51 -0400197 def find(self, path='/'):
198 return self.try_mapper_call(
199 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400200
Brad Bishop87b63c12016-03-18 14:47:51 -0400201 def setup(self, path='/'):
202 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400203
Brad Bishop87b63c12016-03-18 14:47:51 -0400204 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400205 return {x: y for x, y in self.mapper.enumerate_subtree(
206 path,
207 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400208
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400209
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500210class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400211 verbs = 'POST'
212 rules = '<path:path>/action/<method>'
213 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400214
Brad Bishop87b63c12016-03-18 14:47:51 -0400215 def __init__(self, app, bus):
216 super(MethodHandler, self).__init__(
217 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400218
Brad Bishop87b63c12016-03-18 14:47:51 -0400219 def find(self, path, method):
220 busses = self.try_mapper_call(
221 self.mapper.get_object, path=path)
222 for items in busses.iteritems():
223 m = self.find_method_on_bus(path, method, *items)
224 if m:
225 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400226
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400228
Brad Bishop87b63c12016-03-18 14:47:51 -0400229 def setup(self, path, method):
230 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400231
Brad Bishop87b63c12016-03-18 14:47:51 -0400232 def do_post(self, path, method):
233 try:
234 if request.parameter_list:
235 return request.route_data['method'](*request.parameter_list)
236 else:
237 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400238
Brad Bishop87b63c12016-03-18 14:47:51 -0400239 except dbus.exceptions.DBusException, e:
240 if e.get_dbus_name() == DBUS_INVALID_ARGS:
241 abort(400, str(e))
242 if e.get_dbus_name() == DBUS_TYPE_ERROR:
243 abort(400, str(e))
244 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400245
Brad Bishop87b63c12016-03-18 14:47:51 -0400246 @staticmethod
247 def find_method_in_interface(method, obj, interface, methods):
248 if methods is None:
249 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400250
Brad Bishop6d190602016-04-15 13:09:39 -0400251 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400252 if method is not None:
253 iface = dbus.Interface(obj, interface)
254 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400255
Brad Bishop87b63c12016-03-18 14:47:51 -0400256 def find_method_on_bus(self, path, method, bus, interfaces):
257 obj = self.bus.get_object(bus, path, introspect=False)
258 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
259 data = iface.Introspect()
260 parser = IntrospectionNodeParser(
261 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500262 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 for x, y in parser.get_interfaces().iteritems():
264 m = self.find_method_in_interface(
265 method, obj, x, y.get('method'))
266 if m:
267 return m
268
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400269
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500270class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400271 verbs = ['PUT', 'GET']
272 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishop87b63c12016-03-18 14:47:51 -0400274 def __init__(self, app, bus):
275 super(PropertyHandler, self).__init__(
276 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400277
Brad Bishop87b63c12016-03-18 14:47:51 -0400278 def find(self, path, prop):
279 self.app.instance_handler.setup(path)
280 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500281 real_name = obmc.utils.misc.find_case_insensitive(
282 prop, obj.keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400283
Brad Bishop56ad87f2017-02-21 23:33:29 -0500284 if not real_name:
285 if request.method == 'PUT':
286 abort(403, _4034_msg % ('property', 'created', prop))
287 else:
288 abort(404, _4034_msg % ('property', 'found', prop))
289 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500290
Brad Bishop87b63c12016-03-18 14:47:51 -0400291 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500292 name, obj = self.find(path, prop)
293 request.route_data['obj'] = obj
294 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500295
Brad Bishop87b63c12016-03-18 14:47:51 -0400296 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500297 name = request.route_data['name']
298 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500299
Brad Bishop87b63c12016-03-18 14:47:51 -0400300 def do_put(self, path, prop, value=None):
301 if value is None:
302 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500303
Brad Bishop87b63c12016-03-18 14:47:51 -0400304 prop, iface, properties_iface = self.get_host_interface(
305 path, prop, request.route_data['map'][path])
306 try:
307 properties_iface.Set(iface, prop, value)
308 except ValueError, e:
309 abort(400, str(e))
310 except dbus.exceptions.DBusException, e:
311 if e.get_dbus_name() == DBUS_INVALID_ARGS:
312 abort(403, str(e))
313 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500314
Brad Bishop87b63c12016-03-18 14:47:51 -0400315 def get_host_interface(self, path, prop, bus_info):
316 for bus, interfaces in bus_info.iteritems():
317 obj = self.bus.get_object(bus, path, introspect=True)
318 properties_iface = dbus.Interface(
319 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500320
Brad Bishop87b63c12016-03-18 14:47:51 -0400321 info = self.get_host_interface_on_bus(
322 path, prop, properties_iface, bus, interfaces)
323 if info is not None:
324 prop, iface = info
325 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500326
Brad Bishop87b63c12016-03-18 14:47:51 -0400327 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
328 for i in interfaces:
329 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500330 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 continue
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500332 prop = obmc.utils.misc.find_case_insensitive(
333 prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400334 if prop is None:
335 continue
336 return prop, i
337
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500338
Brad Bishop2503bd62015-12-16 17:56:12 -0500339class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 verbs = ['GET']
341 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def __init__(self, app, bus):
344 super(SchemaHandler, self).__init__(
345 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500346
Brad Bishop87b63c12016-03-18 14:47:51 -0400347 def find(self, path):
348 return self.try_mapper_call(
349 self.mapper.get_object,
350 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500351
Brad Bishop87b63c12016-03-18 14:47:51 -0400352 def setup(self, path):
353 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500354
Brad Bishop87b63c12016-03-18 14:47:51 -0400355 def do_get(self, path):
356 schema = {}
357 for x in request.route_data['map'].iterkeys():
358 obj = self.bus.get_object(x, path, introspect=False)
359 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
360 data = iface.Introspect()
361 parser = IntrospectionNodeParser(
362 ElementTree.fromstring(data))
363 for x, y in parser.get_interfaces().iteritems():
364 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500365
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 return schema
367
Brad Bishop2503bd62015-12-16 17:56:12 -0500368
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500369class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400370 verbs = ['GET', 'PUT', 'DELETE']
371 rules = '<path:path>'
372 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500373
Brad Bishop87b63c12016-03-18 14:47:51 -0400374 def __init__(self, app, bus):
375 super(InstanceHandler, self).__init__(
376 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 def find(self, path, callback=None):
379 return {path: self.try_mapper_call(
380 self.mapper.get_object,
381 callback,
382 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500383
Brad Bishop87b63c12016-03-18 14:47:51 -0400384 def setup(self, path):
385 callback = None
386 if request.method == 'PUT':
387 def callback(e, **kw):
388 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500389
Brad Bishop87b63c12016-03-18 14:47:51 -0400390 if request.route_data.get('map') is None:
391 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500392
Brad Bishop87b63c12016-03-18 14:47:51 -0400393 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400394 return self.mapper.enumerate_object(
395 path,
396 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500397
Brad Bishop87b63c12016-03-18 14:47:51 -0400398 def do_put(self, path):
399 # make sure all properties exist in the request
400 obj = set(self.do_get(path).keys())
401 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500402
Brad Bishop87b63c12016-03-18 14:47:51 -0400403 diff = list(obj.difference(req))
404 if diff:
405 abort(403, _4034_msg % (
406 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500407
Brad Bishop87b63c12016-03-18 14:47:51 -0400408 diff = list(req.difference(obj))
409 if diff:
410 abort(403, _4034_msg % (
411 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 for p, v in request.parameter_list.iteritems():
414 self.app.property_handler.do_put(
415 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500416
Brad Bishop87b63c12016-03-18 14:47:51 -0400417 def do_delete(self, path):
418 for bus_info in request.route_data['map'][path].iteritems():
419 if self.bus_missing_delete(path, *bus_info):
420 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500421
Brad Bishop87b63c12016-03-18 14:47:51 -0400422 for bus in request.route_data['map'][path].iterkeys():
423 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500424
Brad Bishop87b63c12016-03-18 14:47:51 -0400425 def bus_missing_delete(self, path, bus, interfaces):
426 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500427
Brad Bishop87b63c12016-03-18 14:47:51 -0400428 def delete_on_bus(self, path, bus):
429 obj = self.bus.get_object(bus, path, introspect=False)
430 delete_iface = dbus.Interface(
431 obj, dbus_interface=DELETE_IFACE)
432 delete_iface.Delete()
433
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500434
Brad Bishop2f428582015-12-02 10:56:11 -0500435class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 ''' Handles the /login and /logout routes, manages
437 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500438
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 rules = ['/login', '/logout']
440 login_str = "User '%s' logged %s"
441 bad_passwd_str = "Invalid username or password"
442 no_user_str = "No user logged in"
443 bad_json_str = "Expecting request format { 'data': " \
444 "[<username>, <password>] }, got '%s'"
445 _require_auth = None
446 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500447
Brad Bishop87b63c12016-03-18 14:47:51 -0400448 def __init__(self, app, bus):
449 super(SessionHandler, self).__init__(
450 app, bus)
451 self.hmac_key = os.urandom(128)
452 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500453
Brad Bishop87b63c12016-03-18 14:47:51 -0400454 @staticmethod
455 def authenticate(username, clear):
456 try:
457 encoded = spwd.getspnam(username)[1]
458 return encoded == crypt.crypt(clear, encoded)
459 except KeyError:
460 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500461
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 def invalidate_session(self, session):
463 try:
464 self.session_store.remove(session)
465 except ValueError:
466 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500467
Brad Bishop87b63c12016-03-18 14:47:51 -0400468 def new_session(self):
469 sid = os.urandom(32)
470 if self.MAX_SESSIONS <= len(self.session_store):
471 self.session_store.pop()
472 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500473
Brad Bishop87b63c12016-03-18 14:47:51 -0400474 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500475
Brad Bishop87b63c12016-03-18 14:47:51 -0400476 def get_session(self, sid):
477 sids = [x['sid'] for x in self.session_store]
478 try:
479 return self.session_store[sids.index(sid)]
480 except ValueError:
481 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500482
Brad Bishop87b63c12016-03-18 14:47:51 -0400483 def get_session_from_cookie(self):
484 return self.get_session(
485 request.get_cookie(
486 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500487
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 def do_post(self, **kw):
489 if request.path == '/login':
490 return self.do_login(**kw)
491 else:
492 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500493
Brad Bishop87b63c12016-03-18 14:47:51 -0400494 def do_logout(self, **kw):
495 session = self.get_session_from_cookie()
496 if session is not None:
497 user = session['user']
498 self.invalidate_session(session)
499 response.delete_cookie('sid')
500 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500501
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500503
Brad Bishop87b63c12016-03-18 14:47:51 -0400504 def do_login(self, **kw):
505 session = self.get_session_from_cookie()
506 if session is not None:
507 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500508
Brad Bishop87b63c12016-03-18 14:47:51 -0400509 if len(request.parameter_list) != 2:
510 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400513 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500514
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 user = request.parameter_list[0]
516 session = self.new_session()
517 session['user'] = user
518 response.set_cookie(
519 'sid', session['sid'], secret=self.hmac_key,
520 secure=True,
521 httponly=True)
522 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500523
Brad Bishop87b63c12016-03-18 14:47:51 -0400524 def find(self, **kw):
525 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500526
Brad Bishop87b63c12016-03-18 14:47:51 -0400527 def setup(self, **kw):
528 pass
529
Brad Bishop2f428582015-12-02 10:56:11 -0500530
531class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400532 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500533
Brad Bishop87b63c12016-03-18 14:47:51 -0400534 name = 'authorization'
535 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500536
Brad Bishop87b63c12016-03-18 14:47:51 -0400537 class Compose:
538 def __init__(self, validators, callback, session_mgr):
539 self.validators = validators
540 self.callback = callback
541 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 def __call__(self, *a, **kw):
544 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
545 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500546 if request.method != 'OPTIONS':
547 for x in self.validators:
548 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500549
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500551
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 def apply(self, callback, route):
553 undecorated = route.get_undecorated_callback()
554 if not isinstance(undecorated, RouteHandler):
555 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500556
Brad Bishop87b63c12016-03-18 14:47:51 -0400557 auth_types = getattr(
558 undecorated, '_require_auth', None)
559 if not auth_types:
560 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500561
Brad Bishop87b63c12016-03-18 14:47:51 -0400562 return self.Compose(
563 auth_types, callback, undecorated.app.session_handler)
564
Brad Bishop2f428582015-12-02 10:56:11 -0500565
Brad Bishopd0c404a2017-02-21 09:23:25 -0500566class CorsPlugin(object):
567 ''' Add CORS headers. '''
568
569 name = 'cors'
570 api = 2
571
572 @staticmethod
573 def process_origin():
574 origin = request.headers.get('Origin')
575 if origin:
576 response.add_header('Access-Control-Allow-Origin', origin)
577 response.add_header(
578 'Access-Control-Allow-Credentials', 'true')
579
580 @staticmethod
581 def process_method_and_headers(verbs):
582 method = request.headers.get('Access-Control-Request-Method')
583 headers = request.headers.get('Access-Control-Request-Headers')
584 if headers:
585 headers = [x.lower() for x in headers.split(',')]
586
587 if method in verbs \
588 and headers == ['content-type']:
589 response.add_header('Access-Control-Allow-Methods', method)
590 response.add_header(
591 'Access-Control-Allow-Headers', 'Content-Type')
592
593 def __init__(self, app):
594 app.install_error_callback(self.error_callback)
595
596 def apply(self, callback, route):
597 undecorated = route.get_undecorated_callback()
598 if not isinstance(undecorated, RouteHandler):
599 return callback
600
601 if not getattr(undecorated, '_enable_cors', None):
602 return callback
603
604 def wrap(*a, **kw):
605 self.process_origin()
606 self.process_method_and_headers(undecorated._verbs)
607 return callback(*a, **kw)
608
609 return wrap
610
611 def error_callback(self, **kw):
612 self.process_origin()
613
614
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500615class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400616 ''' Ensures request content satisfies the OpenBMC json api format. '''
617 name = 'json_api_request'
618 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500619
Brad Bishop87b63c12016-03-18 14:47:51 -0400620 error_str = "Expecting request format { 'data': <value> }, got '%s'"
621 type_error_str = "Unsupported Content-Type: '%s'"
622 json_type = "application/json"
623 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 @staticmethod
626 def content_expected():
627 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500628
Brad Bishop87b63c12016-03-18 14:47:51 -0400629 def validate_request(self):
630 if request.content_length > 0 and \
631 request.content_type != self.json_type:
632 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500633
Brad Bishop87b63c12016-03-18 14:47:51 -0400634 try:
635 request.parameter_list = request.json.get('data')
636 except ValueError, e:
637 abort(400, str(e))
638 except (AttributeError, KeyError, TypeError):
639 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500640
Brad Bishop87b63c12016-03-18 14:47:51 -0400641 def apply(self, callback, route):
642 verbs = getattr(
643 route.get_undecorated_callback(), '_verbs', None)
644 if verbs is None:
645 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500646
Brad Bishop87b63c12016-03-18 14:47:51 -0400647 if not set(self.request_methods).intersection(verbs):
648 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500649
Brad Bishop87b63c12016-03-18 14:47:51 -0400650 def wrap(*a, **kw):
651 if self.content_expected():
652 self.validate_request()
653 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500654
Brad Bishop87b63c12016-03-18 14:47:51 -0400655 return wrap
656
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500657
658class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400659 ''' Ensures request content type satisfies the OpenBMC json api format. '''
660 name = 'json_api_method_request'
661 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500662
Brad Bishop87b63c12016-03-18 14:47:51 -0400663 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500664
Brad Bishop87b63c12016-03-18 14:47:51 -0400665 def apply(self, callback, route):
666 request_type = getattr(
667 route.get_undecorated_callback(), 'request_type', None)
668 if request_type is None:
669 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500670
Brad Bishop87b63c12016-03-18 14:47:51 -0400671 def validate_request():
672 if not isinstance(request.parameter_list, request_type):
673 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500674
Brad Bishop87b63c12016-03-18 14:47:51 -0400675 def wrap(*a, **kw):
676 if JsonApiRequestPlugin.content_expected():
677 validate_request()
678 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500679
Brad Bishop87b63c12016-03-18 14:47:51 -0400680 return wrap
681
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500682
Brad Bishop080a48e2017-02-21 22:34:43 -0500683class JsonErrorsPlugin(JSONPlugin):
684 ''' Extend the Bottle JSONPlugin such that it also encodes error
685 responses. '''
686
687 def __init__(self, app, **kw):
688 super(JsonErrorsPlugin, self).__init__(**kw)
689 self.json_opts = {
690 x: y for x, y in kw.iteritems()
691 if x in ['indent', 'sort_keys']}
692 app.install_error_callback(self.error_callback)
693
694 def error_callback(self, response_object, response_body, **kw):
695 response_body['body'] = json.dumps(response_object, **self.json_opts)
696 response.content_type = 'application/json'
697
698
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500699class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500700 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400701 name = 'json_api_response'
702 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500703
Brad Bishopd4c1c552017-02-21 00:07:28 -0500704 @staticmethod
705 def has_body():
706 return request.method not in ['OPTIONS']
707
Brad Bishop080a48e2017-02-21 22:34:43 -0500708 def __init__(self, app):
709 app.install_error_callback(self.error_callback)
710
Brad Bishop87b63c12016-03-18 14:47:51 -0400711 def apply(self, callback, route):
712 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500713 data = callback(*a, **kw)
714 if self.has_body():
715 resp = {'data': data}
716 resp['status'] = 'ok'
717 resp['message'] = response.status_line
718 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400719 return wrap
720
Brad Bishop080a48e2017-02-21 22:34:43 -0500721 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400722 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -0500723 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -0500724 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400725 if error.status_code == 500:
726 response_object['data']['exception'] = repr(error.exception)
727 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500728
Brad Bishop87b63c12016-03-18 14:47:51 -0400729
Brad Bishop080a48e2017-02-21 22:34:43 -0500730class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400731 ''' Json javascript wrapper. '''
732 name = 'jsonp'
733 api = 2
734
Brad Bishop080a48e2017-02-21 22:34:43 -0500735 def __init__(self, app, **kw):
736 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400737
738 @staticmethod
739 def to_jsonp(json):
740 jwrapper = request.query.callback or None
741 if(jwrapper):
742 response.set_header('Content-Type', 'application/javascript')
743 json = jwrapper + '(' + json + ');'
744 return json
745
746 def apply(self, callback, route):
747 def wrap(*a, **kw):
748 return self.to_jsonp(callback(*a, **kw))
749 return wrap
750
Brad Bishop080a48e2017-02-21 22:34:43 -0500751 def error_callback(self, response_body, **kw):
752 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400753
754
Brad Bishop2c6fc762016-08-29 15:53:25 -0400755class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400756 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400757 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400758 self.bus = dbus.SystemBus()
759 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -0500760 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500761
Brad Bishop87b63c12016-03-18 14:47:51 -0400762 self.install_hooks()
763 self.install_plugins()
764 self.create_handlers()
765 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500766
Brad Bishop87b63c12016-03-18 14:47:51 -0400767 def install_plugins(self):
768 # install json api plugins
769 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400770 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -0500771 self.install(CorsPlugin(self))
Brad Bishop080a48e2017-02-21 22:34:43 -0500772 self.install(JsonpPlugin(self, **json_kw))
773 self.install(JsonErrorsPlugin(self, **json_kw))
774 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -0400775 self.install(JsonApiRequestPlugin())
776 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500777
Brad Bishop87b63c12016-03-18 14:47:51 -0400778 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -0500779 self.error_handler_type = type(self.default_error_handler)
780 self.original_error_handler = self.default_error_handler
781 self.default_error_handler = self.error_handler_type(
782 self.custom_error_handler, self, Bottle)
783
Brad Bishop87b63c12016-03-18 14:47:51 -0400784 self.real_router_match = self.router.match
785 self.router.match = self.custom_router_match
786 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500787
Brad Bishop87b63c12016-03-18 14:47:51 -0400788 def create_handlers(self):
789 # create route handlers
790 self.session_handler = SessionHandler(self, self.bus)
791 self.directory_handler = DirectoryHandler(self, self.bus)
792 self.list_names_handler = ListNamesHandler(self, self.bus)
793 self.list_handler = ListHandler(self, self.bus)
794 self.method_handler = MethodHandler(self, self.bus)
795 self.property_handler = PropertyHandler(self, self.bus)
796 self.schema_handler = SchemaHandler(self, self.bus)
797 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500798
Brad Bishop87b63c12016-03-18 14:47:51 -0400799 def install_handlers(self):
800 self.session_handler.install()
801 self.directory_handler.install()
802 self.list_names_handler.install()
803 self.list_handler.install()
804 self.method_handler.install()
805 self.property_handler.install()
806 self.schema_handler.install()
807 # this has to come last, since it matches everything
808 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500809
Brad Bishop080a48e2017-02-21 22:34:43 -0500810 def install_error_callback(self, callback):
811 self.error_callbacks.insert(0, callback)
812
Brad Bishop87b63c12016-03-18 14:47:51 -0400813 def custom_router_match(self, environ):
814 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
815 needed doesn't work for us since the instance rules match
816 everything. This monkey-patch lets the route handler figure
817 out which response is needed. This could be accomplished
818 with a hook but that would require calling the router match
819 function twice.
820 '''
821 route, args = self.real_router_match(environ)
822 if isinstance(route.callback, RouteHandler):
823 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500824
Brad Bishop87b63c12016-03-18 14:47:51 -0400825 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500826
Brad Bishop080a48e2017-02-21 22:34:43 -0500827 def custom_error_handler(self, res, error):
828 ''' Allow plugins to modify error reponses too via this custom
829 error handler. '''
830
831 response_object = {}
832 response_body = {}
833 for x in self.error_callbacks:
834 x(error=error,
835 response_object=response_object,
836 response_body=response_body)
837
838 return response_body.get('body', "")
839
Brad Bishop87b63c12016-03-18 14:47:51 -0400840 @staticmethod
841 def strip_extra_slashes():
842 path = request.environ['PATH_INFO']
843 trailing = ("", "/")[path[-1] == '/']
844 parts = filter(bool, path.split('/'))
845 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing