blob: a26532a7b71b63877c6fbd1d2b9b87cae0273853 [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
Alexander Filippovd08a4562018-03-20 12:02:23 +030018import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -040019import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050020import dbus.exceptions
21import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050023from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Jayanth Othayoth9bc94992017-06-29 06:30:40 -050024from bottle import static_file
Brad Bishopb103d2d2016-03-04 16:19:14 -050025import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050026from obmc.dbuslib.introspection import IntrospectionNodeParser
27import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050028import spwd
29import grp
30import crypt
Deepak Kodihalli1af301a2017-04-11 07:29:01 -050031import tempfile
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050032import re
Matt Spinlerd41643e2018-02-02 13:51:38 -060033import mimetypes
Deepak Kodihalli639b5022017-10-13 06:40:26 -050034have_wsock = True
35try:
36 from geventwebsocket import WebSocketError
37except ImportError:
38 have_wsock = False
39if have_wsock:
40 from dbus.mainloop.glib import DBusGMainLoop
41 DBusGMainLoop(set_as_default=True)
CamVan Nguyen249d1322018-03-05 10:08:33 -060042 # TODO: openbmc/openbmc#2994 remove python 2 support
43 try: # python 2
44 import gobject
45 except ImportError: # python 3
46 from gi.repository import GObject as gobject
Deepak Kodihalli639b5022017-10-13 06:40:26 -050047 import gevent
Deepak Kodihalli5c518f62018-04-23 03:26:38 -050048 from gevent import socket
49 from gevent import Greenlet
Brad Bishopaa65f6e2015-10-27 16:28:51 -040050
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -060051DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050052DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
Adriana Kobylaka8b05d12018-08-23 10:44:07 -050053DBUS_PROPERTY_READONLY = 'org.freedesktop.DBus.Error.PropertyReadOnly'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050054DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050055DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050056DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Adriana Kobylak53693892018-03-12 13:05:50 -050057SOFTWARE_PATH = '/xyz/openbmc_project/software'
Jayashankar Padathbec10c22018-05-29 18:22:59 +053058WEBSOCKET_TIMEOUT = 45
Brad Bishop9ee57c42015-11-03 14:59:29 -050059
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050060_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040061
Matt Spinlerd41643e2018-02-02 13:51:38 -060062www_base_path = '/usr/share/www/'
63
Brad Bishop87b63c12016-03-18 14:47:51 -040064
Brad Bishop2f428582015-12-02 10:56:11 -050065def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040066 ''' Authorization plugin callback that checks
67 that the user is logged in. '''
68 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040069 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040070
Brad Bishop2f428582015-12-02 10:56:11 -050071
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050072def get_type_signature_by_introspection(bus, service, object_path,
73 property_name):
74 obj = bus.get_object(service, object_path)
75 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
76 xml_string = iface.Introspect()
77 for child in ElementTree.fromstring(xml_string):
78 # Iterate over each interfaces's properties to find
79 # matching property_name, and return its signature string
80 if child.tag == 'interface':
81 for i in child.iter():
82 if ('name' in i.attrib) and \
83 (i.attrib['name'] == property_name):
84 type_signature = i.attrib['type']
85 return type_signature
86
87
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053088def get_method_signature(bus, service, object_path, interface, method):
89 obj = bus.get_object(service, object_path)
90 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
91 xml_string = iface.Introspect()
92 arglist = []
93
94 root = ElementTree.fromstring(xml_string)
95 for dbus_intf in root.findall('interface'):
96 if (dbus_intf.get('name') == interface):
97 for dbus_method in dbus_intf.findall('method'):
98 if(dbus_method.get('name') == method):
99 for arg in dbus_method.findall('arg'):
100 arglist.append(arg.get('type'))
101 return arglist
102
103
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500104def split_struct_signature(signature):
105 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
106 struct_matches = re.findall(struct_regex, signature)
107 return struct_matches
108
109
110def convert_type(signature, value):
111 # Basic Types
112 converted_value = None
113 converted_container = None
CamVan Nguyen249d1322018-03-05 10:08:33 -0600114 # TODO: openbmc/openbmc#2994 remove python 2 support
115 try: # python 2
116 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
117 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
118 't': dbus.UInt64, 'd': float, 's': str}
119 except NameError: # python 3
120 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
121 'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32,
122 't': dbus.UInt64, 'd': float, 's': str}
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500123 array_matches = re.match(r'a\((\S+)\)', signature)
124 struct_matches = re.match(r'\((\S+)\)', signature)
125 dictionary_matches = re.match(r'a{(\S+)}', signature)
126 if signature in basic_types:
127 converted_value = basic_types[signature](value)
128 return converted_value
129 # Array
130 if array_matches:
131 element_type = array_matches.group(1)
132 converted_container = list()
133 # Test if value is a list
134 # to avoid iterating over each character in a string.
135 # Iterate over each item and convert type
136 if isinstance(value, list):
137 for i in value:
138 converted_element = convert_type(element_type, i)
139 converted_container.append(converted_element)
140 # Convert non-sequence to expected type, and append to list
141 else:
142 converted_element = convert_type(element_type, value)
143 converted_container.append(converted_element)
144 return converted_container
145 # Struct
146 if struct_matches:
147 element_types = struct_matches.group(1)
148 split_element_types = split_struct_signature(element_types)
149 converted_container = list()
150 # Test if value is a list
151 if isinstance(value, list):
152 for index, val in enumerate(value):
153 converted_element = convert_type(split_element_types[index],
154 value[index])
155 converted_container.append(converted_element)
156 else:
157 converted_element = convert_type(element_types, value)
158 converted_container.append(converted_element)
159 return tuple(converted_container)
160 # Dictionary
161 if dictionary_matches:
162 element_types = dictionary_matches.group(1)
163 split_element_types = split_struct_signature(element_types)
164 converted_container = dict()
165 # Convert each element of dict
CamVan Nguyen249d1322018-03-05 10:08:33 -0600166 for key, val in value.items():
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500167 converted_key = convert_type(split_element_types[0], key)
168 converted_val = convert_type(split_element_types[1], val)
169 converted_container[converted_key] = converted_val
170 return converted_container
171
172
Jayashankar Padathbec10c22018-05-29 18:22:59 +0530173def send_ws_ping(wsock, timeout) :
174 # Most webservers close websockets after 60 seconds of
175 # inactivity. Make sure to send a ping before that.
176 payload = "ping"
177 # the ping payload can be anything, the receiver has to just
178 # return the same back.
179 while True:
180 gevent.sleep(timeout)
181 try:
182 if wsock:
183 wsock.send_frame(payload, wsock.OPCODE_PING)
184 except Exception as e:
185 wsock.close()
186 return
187
188
Brad Bishop2f428582015-12-02 10:56:11 -0500189class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400190 ''' Authorization plugin callback that checks that the user is logged in
191 and a member of a group. '''
192 def __init__(self, group):
193 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500194
Brad Bishop87b63c12016-03-18 14:47:51 -0400195 def __call__(self, session, *a, **kw):
196 valid_user(session, *a, **kw)
197 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500198
Brad Bishop87b63c12016-03-18 14:47:51 -0400199 try:
200 res = session['user'] in grp.getgrnam(self.group)[3]
201 except KeyError:
202 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500203
Brad Bishop87b63c12016-03-18 14:47:51 -0400204 if not res:
205 abort(403, 'Insufficient access')
206
Brad Bishop2f428582015-12-02 10:56:11 -0500207
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500208class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400209 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500210 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400211
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500212 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400213 self.app = app
214 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500215 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400216 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500218 self._content_type = content_type
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400219
Brad Bishop88c76a42017-02-21 00:02:02 -0500220 if 'GET' in self._verbs:
221 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500222 if 'OPTIONS' not in self._verbs:
223 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500224
Brad Bishop87b63c12016-03-18 14:47:51 -0400225 def _setup(self, **kw):
226 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500227
Brad Bishop87b63c12016-03-18 14:47:51 -0400228 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500229 if request.method != 'OPTIONS':
230 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500231
Brad Bishopd4c1c552017-02-21 00:07:28 -0500232 # Javascript implementations will not send credentials
233 # with an OPTIONS request. Don't help malicious clients
234 # by checking the path here and returning a 404 if the
235 # path doesn't exist.
236 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500237
Brad Bishopd4c1c552017-02-21 00:07:28 -0500238 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500239 raise HTTPError(
240 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400241
Brad Bishop87b63c12016-03-18 14:47:51 -0400242 def __call__(self, **kw):
243 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400244
Brad Bishop88c76a42017-02-21 00:02:02 -0500245 def do_head(self, **kw):
246 return self.do_get(**kw)
247
Brad Bishopd4c1c552017-02-21 00:07:28 -0500248 def do_options(self, **kw):
249 for v in self._verbs:
250 response.set_header(
251 'Allow',
252 ','.join(self._verbs))
253 return None
254
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 def install(self):
256 self.app.route(
257 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500258 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400259
Brad Bishop87b63c12016-03-18 14:47:51 -0400260 @staticmethod
261 def try_mapper_call(f, callback=None, **kw):
262 try:
263 return f(**kw)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600264 except dbus.exceptions.DBusException as e:
Brad Bishopfce77562016-11-28 15:44:18 -0500265 if e.get_dbus_name() == \
266 'org.freedesktop.DBus.Error.ObjectPathInUse':
267 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500268 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400269 raise
270 if callback is None:
271 def callback(e, **kw):
272 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishop87b63c12016-03-18 14:47:51 -0400274 callback(e, **kw)
275
276 @staticmethod
277 def try_properties_interface(f, *a):
278 try:
279 return f(*a)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600280 except dbus.exceptions.DBusException as e:
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -0600281 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name():
Brad Bishopf4e74982016-04-01 14:53:05 -0400282 # interface doesn't have any properties
283 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400284 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
285 # properties interface not implemented at all
286 return None
287 raise
288
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500290class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400291 verbs = 'GET'
292 rules = '<path:path>/'
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -0500293 suppress_logging = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400294
Brad Bishop87b63c12016-03-18 14:47:51 -0400295 def __init__(self, app, bus):
296 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400297 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400298
Brad Bishop87b63c12016-03-18 14:47:51 -0400299 def find(self, path='/'):
300 return self.try_mapper_call(
301 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def setup(self, path='/'):
304 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400305
Brad Bishop87b63c12016-03-18 14:47:51 -0400306 def do_get(self, path='/'):
307 return request.route_data['map']
308
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400309
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500310class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400311 verbs = 'GET'
312 rules = ['/list', '<path:path>/list']
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -0500313 suppress_logging = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400314
Brad Bishop87b63c12016-03-18 14:47:51 -0400315 def __init__(self, app, bus):
316 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400317 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400318
Brad Bishop87b63c12016-03-18 14:47:51 -0400319 def find(self, path='/'):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600320 return list(self.try_mapper_call(
321 self.mapper.get_subtree, path=path).keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400322
Brad Bishop87b63c12016-03-18 14:47:51 -0400323 def setup(self, path='/'):
324 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400325
Brad Bishop87b63c12016-03-18 14:47:51 -0400326 def do_get(self, path='/'):
327 return request.route_data['map']
328
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400329
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500330class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 verbs = 'GET'
332 rules = ['/enumerate', '<path:path>/enumerate']
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -0500333 suppress_logging = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400334
Brad Bishop87b63c12016-03-18 14:47:51 -0400335 def __init__(self, app, bus):
336 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400337 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 def find(self, path='/'):
340 return self.try_mapper_call(
341 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def setup(self, path='/'):
344 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400345
Brad Bishop87b63c12016-03-18 14:47:51 -0400346 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400347 return {x: y for x, y in self.mapper.enumerate_subtree(
348 path,
349 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400350
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400351
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500352class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400353 verbs = 'POST'
354 rules = '<path:path>/action/<method>'
355 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500356 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400357
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 def __init__(self, app, bus):
359 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500360 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530361 self.service = ''
362 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400363
Brad Bishop87b63c12016-03-18 14:47:51 -0400364 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500365 method_list = []
Gunnar Mills313aadb2018-04-08 14:50:09 -0500366 buses = self.try_mapper_call(
Brad Bishop87b63c12016-03-18 14:47:51 -0400367 self.mapper.get_object, path=path)
Gunnar Mills313aadb2018-04-08 14:50:09 -0500368 for items in buses.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400369 m = self.find_method_on_bus(path, method, *items)
370 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500371 method_list.append(m)
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600372 if method_list:
373 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400374
Brad Bishop87b63c12016-03-18 14:47:51 -0400375 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400376
Brad Bishop87b63c12016-03-18 14:47:51 -0400377 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500378 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400379
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600380 def do_post(self, path, method, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400381 try:
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600382 args = []
383 if request.parameter_list:
384 args = request.parameter_list
385 # To see if the return type is capable of being merged
386 if len(request.route_data['map']) > 1:
387 results = None
388 for item in request.route_data['map']:
389 tmp = item(*args)
390 if not results:
391 if tmp is not None:
392 results = type(tmp)()
393 if isinstance(results, dict):
394 results = results.update(tmp)
395 elif isinstance(results, list):
396 results = results + tmp
397 elif isinstance(results, type(None)):
398 results = None
399 else:
400 abort(501, 'Don\'t know how to merge method call '
401 'results of {}'.format(type(tmp)))
402 return results
403 # There is only one method
404 return request.route_data['map'][0](*args)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400405
CamVan Nguyen249d1322018-03-05 10:08:33 -0600406 except dbus.exceptions.DBusException as e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530407 paramlist = []
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500408 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530409
410 signature_list = get_method_signature(self.bus, self.service,
411 path, self.interface,
412 method)
413 if not signature_list:
414 abort(400, "Failed to get method signature: %s" % str(e))
415 if len(signature_list) != len(request.parameter_list):
416 abort(400, "Invalid number of args")
417 converted_value = None
418 try:
419 for index, expected_type in enumerate(signature_list):
420 value = request.parameter_list[index]
421 converted_value = convert_type(expected_type, value)
422 paramlist.append(converted_value)
423 request.parameter_list = paramlist
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600424 self.do_post(path, method, False)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530425 return
426 except Exception as ex:
Nagaraju Gorugantiab404fa2017-12-14 10:24:40 -0600427 abort(400, "Bad Request/Invalid Args given")
Brad Bishop87b63c12016-03-18 14:47:51 -0400428 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 if e.get_dbus_name() == DBUS_TYPE_ERROR:
431 abort(400, str(e))
432 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400433
Brad Bishop87b63c12016-03-18 14:47:51 -0400434 @staticmethod
435 def find_method_in_interface(method, obj, interface, methods):
436 if methods is None:
437 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400438
CamVan Nguyen249d1322018-03-05 10:08:33 -0600439 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
Brad Bishop87b63c12016-03-18 14:47:51 -0400440 if method is not None:
441 iface = dbus.Interface(obj, interface)
442 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400443
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 def find_method_on_bus(self, path, method, bus, interfaces):
445 obj = self.bus.get_object(bus, path, introspect=False)
446 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
447 data = iface.Introspect()
448 parser = IntrospectionNodeParser(
449 ElementTree.fromstring(data),
Brad Bishopaeb995d2018-04-04 22:28:42 -0400450 intf_match=lambda x: x in interfaces)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600451 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400452 m = self.find_method_in_interface(
453 method, obj, x, y.get('method'))
454 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530455 self.service = bus
456 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400457 return m
458
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400459
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500460class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400461 verbs = ['PUT', 'GET']
462 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500463 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def __init__(self, app, bus):
466 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500467 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400468
Brad Bishop87b63c12016-03-18 14:47:51 -0400469 def find(self, path, prop):
470 self.app.instance_handler.setup(path)
471 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500472 real_name = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600473 prop, list(obj.keys()))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400474
Brad Bishop56ad87f2017-02-21 23:33:29 -0500475 if not real_name:
476 if request.method == 'PUT':
477 abort(403, _4034_msg % ('property', 'created', prop))
478 else:
479 abort(404, _4034_msg % ('property', 'found', prop))
480 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500481
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500483 name, obj = self.find(path, prop)
484 request.route_data['obj'] = obj
485 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500486
Brad Bishop87b63c12016-03-18 14:47:51 -0400487 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500488 name = request.route_data['name']
489 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500490
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600491 def do_put(self, path, prop, value=None, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400492 if value is None:
493 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500494
Brad Bishop87b63c12016-03-18 14:47:51 -0400495 prop, iface, properties_iface = self.get_host_interface(
496 path, prop, request.route_data['map'][path])
497 try:
498 properties_iface.Set(iface, prop, value)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600499 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 abort(400, str(e))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600501 except dbus.exceptions.DBusException as e:
Adriana Kobylaka8b05d12018-08-23 10:44:07 -0500502 if e.get_dbus_name() == DBUS_PROPERTY_READONLY:
503 abort(403, str(e))
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500504 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500505 bus_name = properties_iface.bus_name
506 expected_type = get_type_signature_by_introspection(self.bus,
507 bus_name,
508 path,
509 prop)
510 if not expected_type:
511 abort(403, "Failed to get expected type: %s" % str(e))
512 converted_value = None
513 try:
514 converted_value = convert_type(expected_type, value)
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500515 except Exception as ex:
516 abort(403, "Failed to convert %s to type %s" %
517 (value, expected_type))
Lei YU1eea5c32018-07-12 15:32:37 +0800518 try:
519 self.do_put(path, prop, converted_value, False)
520 return
521 except Exception as ex:
522 abort(403, str(ex))
523
Brad Bishop87b63c12016-03-18 14:47:51 -0400524 abort(403, str(e))
525 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500526
Brad Bishop87b63c12016-03-18 14:47:51 -0400527 def get_host_interface(self, path, prop, bus_info):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600528 for bus, interfaces in bus_info.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 obj = self.bus.get_object(bus, path, introspect=True)
530 properties_iface = dbus.Interface(
531 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500532
Adriana Kobylak44573ab2019-01-14 12:54:43 -0600533 try:
534 info = self.get_host_interface_on_bus(
535 path, prop, properties_iface, bus, interfaces)
536 except Exception:
537 continue
Brad Bishop87b63c12016-03-18 14:47:51 -0400538 if info is not None:
539 prop, iface = info
540 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500541
Brad Bishop87b63c12016-03-18 14:47:51 -0400542 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
543 for i in interfaces:
544 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500545 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400546 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500547 match = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600548 prop, list(properties.keys()))
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500549 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500551 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 return prop, i
553
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500554
Brad Bishop2503bd62015-12-16 17:56:12 -0500555class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400556 verbs = ['GET']
557 rules = '<path:path>/schema'
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -0500558 suppress_logging = True
Brad Bishop2503bd62015-12-16 17:56:12 -0500559
Brad Bishop87b63c12016-03-18 14:47:51 -0400560 def __init__(self, app, bus):
561 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400562 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 def find(self, path):
565 return self.try_mapper_call(
566 self.mapper.get_object,
567 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500568
Brad Bishop87b63c12016-03-18 14:47:51 -0400569 def setup(self, path):
570 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 def do_get(self, path):
573 schema = {}
CamVan Nguyen249d1322018-03-05 10:08:33 -0600574 for x in request.route_data['map'].keys():
Brad Bishop87b63c12016-03-18 14:47:51 -0400575 obj = self.bus.get_object(x, path, introspect=False)
576 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
577 data = iface.Introspect()
578 parser = IntrospectionNodeParser(
579 ElementTree.fromstring(data))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600580 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400581 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500582
Brad Bishop87b63c12016-03-18 14:47:51 -0400583 return schema
584
Brad Bishop2503bd62015-12-16 17:56:12 -0500585
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500586class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400587 verbs = ['GET', 'PUT', 'DELETE']
588 rules = '<path:path>'
589 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500590
Brad Bishop87b63c12016-03-18 14:47:51 -0400591 def __init__(self, app, bus):
592 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400593 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500594
Brad Bishop87b63c12016-03-18 14:47:51 -0400595 def find(self, path, callback=None):
596 return {path: self.try_mapper_call(
597 self.mapper.get_object,
598 callback,
599 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500600
Brad Bishop87b63c12016-03-18 14:47:51 -0400601 def setup(self, path):
602 callback = None
603 if request.method == 'PUT':
604 def callback(e, **kw):
605 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500606
Brad Bishop87b63c12016-03-18 14:47:51 -0400607 if request.route_data.get('map') is None:
608 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500609
Brad Bishop87b63c12016-03-18 14:47:51 -0400610 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400611 return self.mapper.enumerate_object(
612 path,
613 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500614
Brad Bishop87b63c12016-03-18 14:47:51 -0400615 def do_put(self, path):
616 # make sure all properties exist in the request
617 obj = set(self.do_get(path).keys())
618 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500619
Brad Bishop87b63c12016-03-18 14:47:51 -0400620 diff = list(obj.difference(req))
621 if diff:
622 abort(403, _4034_msg % (
623 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 diff = list(req.difference(obj))
626 if diff:
627 abort(403, _4034_msg % (
628 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500629
CamVan Nguyen249d1322018-03-05 10:08:33 -0600630 for p, v in request.parameter_list.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400631 self.app.property_handler.do_put(
632 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500633
Brad Bishop87b63c12016-03-18 14:47:51 -0400634 def do_delete(self, path):
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500635 deleted = False
636 for bus, interfaces in request.route_data['map'][path].items():
637 if self.bus_has_delete(interfaces):
638 self.delete_on_bus(path, bus)
639 deleted = True
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500640
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500641 #It's OK if some objects didn't have a Delete, but not all
642 if not deleted:
643 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500644
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500645 def bus_has_delete(self, interfaces):
646 return DELETE_IFACE in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500647
Brad Bishop87b63c12016-03-18 14:47:51 -0400648 def delete_on_bus(self, path, bus):
649 obj = self.bus.get_object(bus, path, introspect=False)
650 delete_iface = dbus.Interface(
651 obj, dbus_interface=DELETE_IFACE)
652 delete_iface.Delete()
653
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500654
Brad Bishop2f428582015-12-02 10:56:11 -0500655class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400656 ''' Handles the /login and /logout routes, manages
657 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500658
Brad Bishop87b63c12016-03-18 14:47:51 -0400659 rules = ['/login', '/logout']
660 login_str = "User '%s' logged %s"
661 bad_passwd_str = "Invalid username or password"
662 no_user_str = "No user logged in"
663 bad_json_str = "Expecting request format { 'data': " \
664 "[<username>, <password>] }, got '%s'"
Alexander Filippovd08a4562018-03-20 12:02:23 +0300665 bmc_not_ready_str = "BMC is not ready (booting)"
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 _require_auth = None
667 MAX_SESSIONS = 16
Alexander Filippovd08a4562018-03-20 12:02:23 +0300668 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
669 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
670 BMCSTATE_PROPERTY = 'CurrentBMCState'
671 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -0500672 suppress_json_logging = True
Brad Bishop2f428582015-12-02 10:56:11 -0500673
Brad Bishop87b63c12016-03-18 14:47:51 -0400674 def __init__(self, app, bus):
675 super(SessionHandler, self).__init__(
676 app, bus)
677 self.hmac_key = os.urandom(128)
678 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500679
Brad Bishop87b63c12016-03-18 14:47:51 -0400680 @staticmethod
681 def authenticate(username, clear):
682 try:
683 encoded = spwd.getspnam(username)[1]
684 return encoded == crypt.crypt(clear, encoded)
685 except KeyError:
686 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500687
Brad Bishop87b63c12016-03-18 14:47:51 -0400688 def invalidate_session(self, session):
689 try:
690 self.session_store.remove(session)
691 except ValueError:
692 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500693
Brad Bishop87b63c12016-03-18 14:47:51 -0400694 def new_session(self):
695 sid = os.urandom(32)
696 if self.MAX_SESSIONS <= len(self.session_store):
697 self.session_store.pop()
698 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500699
Brad Bishop87b63c12016-03-18 14:47:51 -0400700 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500701
Brad Bishop87b63c12016-03-18 14:47:51 -0400702 def get_session(self, sid):
703 sids = [x['sid'] for x in self.session_store]
704 try:
705 return self.session_store[sids.index(sid)]
706 except ValueError:
707 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500708
Brad Bishop87b63c12016-03-18 14:47:51 -0400709 def get_session_from_cookie(self):
710 return self.get_session(
711 request.get_cookie(
712 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 def do_post(self, **kw):
715 if request.path == '/login':
716 return self.do_login(**kw)
717 else:
718 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500719
Brad Bishop87b63c12016-03-18 14:47:51 -0400720 def do_logout(self, **kw):
721 session = self.get_session_from_cookie()
722 if session is not None:
723 user = session['user']
724 self.invalidate_session(session)
725 response.delete_cookie('sid')
726 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500727
Brad Bishop87b63c12016-03-18 14:47:51 -0400728 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500729
Brad Bishop87b63c12016-03-18 14:47:51 -0400730 def do_login(self, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400731 if len(request.parameter_list) != 2:
732 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500733
Brad Bishop87b63c12016-03-18 14:47:51 -0400734 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400735 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500736
Alexander Filippovd08a4562018-03-20 12:02:23 +0300737 force = False
738 try:
739 force = request.json.get('force')
740 except (ValueError, AttributeError, KeyError, TypeError):
741 force = False
742
743 if not force and not self.is_bmc_ready():
744 abort(503, self.bmc_not_ready_str)
745
Brad Bishop87b63c12016-03-18 14:47:51 -0400746 user = request.parameter_list[0]
747 session = self.new_session()
748 session['user'] = user
749 response.set_cookie(
750 'sid', session['sid'], secret=self.hmac_key,
751 secure=True,
752 httponly=True)
753 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500754
Alexander Filippovd08a4562018-03-20 12:02:23 +0300755 def is_bmc_ready(self):
756 if not self.app.with_bmc_check:
757 return True
758
759 try:
760 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
761 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
762 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
763 if state == self.BMCSTATE_READY:
764 return True
765
766 except dbus.exceptions.DBusException:
767 pass
768
769 return False
770
Brad Bishop87b63c12016-03-18 14:47:51 -0400771 def find(self, **kw):
772 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500773
Brad Bishop87b63c12016-03-18 14:47:51 -0400774 def setup(self, **kw):
775 pass
776
Brad Bishop2f428582015-12-02 10:56:11 -0500777
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500778class ImageUploadUtils:
779 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500780
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500781 file_loc = '/tmp/images'
782 file_prefix = 'img'
783 file_suffix = ''
Adriana Kobylak53693892018-03-12 13:05:50 -0500784 signal = None
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500785
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500786 @classmethod
787 def do_upload(cls, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500788 def cleanup():
789 os.close(handle)
790 if cls.signal:
791 cls.signal.remove()
792 cls.signal = None
793
794 def signal_callback(path, a, **kw):
795 # Just interested on the first Version interface created which is
796 # triggered when the file is uploaded. This helps avoid getting the
797 # wrong information for multiple upload requests in a row.
798 if "xyz.openbmc_project.Software.Version" in a and \
799 "xyz.openbmc_project.Software.Activation" not in a:
800 paths.append(path)
801
802 while cls.signal:
803 # Serialize uploads by waiting for the signal to be cleared.
804 # This makes it easier to ensure that the version information
805 # is the right one instead of the data from another upload request.
806 gevent.sleep(1)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500807 if not os.path.exists(cls.file_loc):
Gunnar Millsfb515792017-11-09 15:52:17 -0600808 abort(500, "Error Directory not found")
Adriana Kobylak53693892018-03-12 13:05:50 -0500809 paths = []
810 bus = dbus.SystemBus()
811 cls.signal = bus.add_signal_receiver(
812 signal_callback,
813 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
814 signal_name='InterfacesAdded',
815 path=SOFTWARE_PATH)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500816 if not filename:
817 handle, filename = tempfile.mkstemp(cls.file_suffix,
818 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500819 else:
820 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500821 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500822 try:
823 file_contents = request.body.read()
824 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500825 os.write(handle, file_contents)
Adriana Kobylak53693892018-03-12 13:05:50 -0500826 # Close file after writing, the image manager process watches for
827 # the close event to know the upload is complete.
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500828 os.close(handle)
Adriana Kobylak53693892018-03-12 13:05:50 -0500829 except (IOError, ValueError) as e:
830 cleanup()
831 abort(400, str(e))
832 except Exception:
833 cleanup()
834 abort(400, "Unexpected Error")
835 loop = gobject.MainLoop()
836 gcontext = loop.get_context()
837 count = 0
838 version_id = ''
839 while loop is not None:
840 try:
841 if gcontext.pending():
842 gcontext.iteration()
843 if not paths:
844 gevent.sleep(1)
845 else:
846 version_id = os.path.basename(paths.pop())
847 break
848 count += 1
849 if count == 10:
850 break
851 except Exception:
852 break
853 cls.signal.remove()
854 cls.signal = None
Adriana Kobylak97fe4352018-04-10 10:44:11 -0500855 if version_id:
856 return version_id
857 else:
858 abort(400, "Version already exists or failed to be extracted")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500859
860
861class ImagePostHandler(RouteHandler):
862 ''' Handles the /upload/image route. '''
863
864 verbs = ['POST']
865 rules = ['/upload/image']
866 content_type = 'application/octet-stream'
867
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500868 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500869 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500870 app, bus, self.verbs, self.rules, self.content_type)
871
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500872 def do_post(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500873 return ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500874
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500875 def find(self, **kw):
876 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500877
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500878 def setup(self, **kw):
879 pass
880
881
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500882class CertificateHandler:
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500883 file_suffix = '.pem'
884 file_prefix = 'cert_'
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500885 CERT_PATH = '/xyz/openbmc_project/certs'
886 CERT_IFACE = 'xyz.openbmc_project.Certs.Install'
887
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500888 def __init__(self, route_handler, cert_type, service):
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500889 if not service:
890 abort(500, "Missing service")
891 if not cert_type:
892 abort(500, "Missing certificate type")
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500893 bus = dbus.SystemBus()
894 certPath = self.CERT_PATH + "/" + cert_type + "/" + service
895 intfs = route_handler.try_mapper_call(
896 route_handler.mapper.get_object, path=certPath)
897 for busName,intf in intfs.items():
898 if self.CERT_IFACE in intf:
899 self.obj = bus.get_object(busName, certPath)
900 return
901 abort(404, "Path not found")
902
903 def do_upload(self):
904 def cleanup():
905 if os.path.exists(temp.name):
906 os.remove(temp.name)
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500907
908 with tempfile.NamedTemporaryFile(
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500909 suffix=self.file_suffix,
910 prefix=self.file_prefix,
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500911 delete=False) as temp:
912 try:
913 file_contents = request.body.read()
914 request.body.close()
915 temp.write(file_contents)
916 except (IOError, ValueError) as e:
917 cleanup()
918 abort(500, str(e))
919 except Exception:
920 cleanup()
921 abort(500, "Unexpected Error")
922
923 try:
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500924 iface = dbus.Interface(self.obj, self.CERT_IFACE)
925 iface.Install(temp.name)
Deepak Kodihallic043cdd2018-10-02 06:27:57 -0500926 except Exception as e:
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500927 cleanup()
Deepak Kodihalli844bb4e2018-10-03 04:59:26 -0500928 abort(400, str(e))
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500929 cleanup()
930
931 def do_delete(self):
932 delete_iface = dbus.Interface(
933 self.obj, dbus_interface=DELETE_IFACE)
934 delete_iface.Delete()
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500935
936
937class CertificatePutHandler(RouteHandler):
938 ''' Handles the /xyz/openbmc_project/certs/<cert_type>/<service> route. '''
939
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500940 verbs = ['PUT', 'DELETE']
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500941 rules = ['/xyz/openbmc_project/certs/<cert_type>/<service>']
942 content_type = 'application/octet-stream'
943
944 def __init__(self, app, bus):
945 super(CertificatePutHandler, self).__init__(
946 app, bus, self.verbs, self.rules, self.content_type)
947
948 def do_put(self, cert_type, service):
Deepak Kodihallia324acd2018-09-30 06:57:57 -0500949 return CertificateHandler(self, cert_type, service).do_upload()
950
951 def do_delete(self, cert_type, service):
952 return CertificateHandler(self, cert_type, service).do_delete()
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -0500953
954 def find(self, **kw):
955 pass
956
957 def setup(self, **kw):
958 pass
959
960
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500961class EventNotifier:
962 keyNames = {}
963 keyNames['event'] = 'event'
964 keyNames['path'] = 'path'
965 keyNames['intfMap'] = 'interfaces'
966 keyNames['propMap'] = 'properties'
967 keyNames['intf'] = 'interface'
968
969 def __init__(self, wsock, filters):
970 self.wsock = wsock
971 self.paths = filters.get("paths", [])
972 self.interfaces = filters.get("interfaces", [])
Andrew Geissler0f7019d2018-10-10 15:00:17 -0500973 self.signals = []
974 self.socket_error = False
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500975 if not self.paths:
976 self.paths.append(None)
977 bus = dbus.SystemBus()
978 # Add a signal receiver for every path the client is interested in
979 for path in self.paths:
Andrew Geissler0f7019d2018-10-10 15:00:17 -0500980 add_sig = bus.add_signal_receiver(
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500981 self.interfaces_added_handler,
982 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
983 signal_name='InterfacesAdded',
984 path=path)
Andrew Geissler0f7019d2018-10-10 15:00:17 -0500985 chg_sig = bus.add_signal_receiver(
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500986 self.properties_changed_handler,
987 dbus_interface=dbus.PROPERTIES_IFACE,
988 signal_name='PropertiesChanged',
989 path=path,
990 path_keyword='path')
Andrew Geissler0f7019d2018-10-10 15:00:17 -0500991 self.signals.append(add_sig)
992 self.signals.append(chg_sig)
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500993 loop = gobject.MainLoop()
994 # gobject's mainloop.run() will block the entire process, so the gevent
995 # scheduler and hence greenlets won't execute. The while-loop below
996 # works around this limitation by using gevent's sleep, instead of
997 # calling loop.run()
998 gcontext = loop.get_context()
999 while loop is not None:
1000 try:
Andrew Geissler0f7019d2018-10-10 15:00:17 -05001001 if self.socket_error:
1002 for signal in self.signals:
1003 signal.remove()
1004 loop.quit()
1005 break;
Deepak Kodihalli639b5022017-10-13 06:40:26 -05001006 if gcontext.pending():
1007 gcontext.iteration()
1008 else:
1009 # gevent.sleep puts only the current greenlet to sleep,
1010 # not the entire process.
1011 gevent.sleep(5)
1012 except WebSocketError:
1013 break
1014
1015 def interfaces_added_handler(self, path, iprops, **kw):
1016 ''' If the client is interested in these changes, respond to the
1017 client. This handles d-bus interface additions.'''
1018 if (not self.interfaces) or \
1019 (not set(iprops).isdisjoint(self.interfaces)):
1020 response = {}
1021 response[self.keyNames['event']] = "InterfacesAdded"
1022 response[self.keyNames['path']] = path
1023 response[self.keyNames['intfMap']] = iprops
1024 try:
1025 self.wsock.send(json.dumps(response))
Andrew Geissler0f7019d2018-10-10 15:00:17 -05001026 except:
1027 self.socket_error = True
Deepak Kodihalli639b5022017-10-13 06:40:26 -05001028 return
1029
1030 def properties_changed_handler(self, interface, new, old, **kw):
1031 ''' If the client is interested in these changes, respond to the
1032 client. This handles d-bus property changes. '''
1033 if (not self.interfaces) or (interface in self.interfaces):
1034 path = str(kw['path'])
1035 response = {}
1036 response[self.keyNames['event']] = "PropertiesChanged"
1037 response[self.keyNames['path']] = path
1038 response[self.keyNames['intf']] = interface
1039 response[self.keyNames['propMap']] = new
1040 try:
1041 self.wsock.send(json.dumps(response))
Andrew Geissler0f7019d2018-10-10 15:00:17 -05001042 except:
1043 self.socket_error = True
Deepak Kodihalli639b5022017-10-13 06:40:26 -05001044 return
1045
1046
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001047class EventHandler(RouteHandler):
1048 ''' Handles the /subscribe route, for clients to be able
1049 to subscribe to BMC events. '''
1050
1051 verbs = ['GET']
1052 rules = ['/subscribe']
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001053 suppress_logging = True
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001054
1055 def __init__(self, app, bus):
1056 super(EventHandler, self).__init__(
1057 app, bus, self.verbs, self.rules)
1058
1059 def find(self, **kw):
1060 pass
1061
1062 def setup(self, **kw):
1063 pass
1064
1065 def do_get(self):
1066 wsock = request.environ.get('wsgi.websocket')
1067 if not wsock:
1068 abort(400, 'Expected WebSocket request.')
Jayashankar Padathbec10c22018-05-29 18:22:59 +05301069 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli639b5022017-10-13 06:40:26 -05001070 filters = wsock.receive()
1071 filters = json.loads(filters)
1072 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001073
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001074class HostConsoleHandler(RouteHandler):
1075 ''' Handles the /console route, for clients to be able
1076 read/write the host serial console. The way this is
1077 done is by exposing a websocket that's mirrored to an
1078 abstract UNIX domain socket, which is the source for
1079 the console data. '''
1080
1081 verbs = ['GET']
1082 # Naming the route console0, because the numbering will help
1083 # on multi-bmc/multi-host systems.
1084 rules = ['/console0']
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001085 suppress_logging = True
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001086
1087 def __init__(self, app, bus):
1088 super(HostConsoleHandler, self).__init__(
1089 app, bus, self.verbs, self.rules)
1090
1091 def find(self, **kw):
1092 pass
1093
1094 def setup(self, **kw):
1095 pass
1096
1097 def read_wsock(self, wsock, sock):
1098 while True:
1099 try:
1100 incoming = wsock.receive()
1101 if incoming:
1102 # Read websocket, write to UNIX socket
1103 sock.send(incoming)
1104 except Exception as e:
1105 sock.close()
1106 return
1107
1108 def read_sock(self, sock, wsock):
1109 max_sock_read_len = 4096
1110 while True:
1111 try:
1112 outgoing = sock.recv(max_sock_read_len)
1113 if outgoing:
1114 # Read UNIX socket, write to websocket
1115 wsock.send(outgoing)
1116 except Exception as e:
1117 wsock.close()
1118 return
1119
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001120 def do_get(self):
1121 wsock = request.environ.get('wsgi.websocket')
1122 if not wsock:
1123 abort(400, 'Expected WebSocket based request.')
1124
Vernon Mauerydbc46912018-12-19 10:33:46 -08001125 # An abstract Unix socket path must be less than or equal to 108 bytes
1126 # and does not need to be nul-terminated or padded out to 108 bytes
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001127 socket_name = "\0obmc-console"
Vernon Mauerydbc46912018-12-19 10:33:46 -08001128 socket_path = socket_name
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001129 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1130
1131 try:
1132 sock.connect(socket_path)
1133 except Exception as e:
1134 abort(500, str(e))
1135
1136 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1137 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
Jayashankar Padathbec10c22018-05-29 18:22:59 +05301138 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001139 gevent.joinall([wsock_reader, sock_reader, ping_sender])
1140
1141
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001142class ImagePutHandler(RouteHandler):
1143 ''' Handles the /upload/image/<filename> route. '''
1144
1145 verbs = ['PUT']
1146 rules = ['/upload/image/<filename>']
1147 content_type = 'application/octet-stream'
1148
1149 def __init__(self, app, bus):
1150 super(ImagePutHandler, self).__init__(
1151 app, bus, self.verbs, self.rules, self.content_type)
1152
1153 def do_put(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -05001154 return ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -05001155
1156 def find(self, **kw):
1157 pass
1158
1159 def setup(self, **kw):
1160 pass
1161
1162
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001163class DownloadDumpHandler(RouteHandler):
1164 ''' Handles the /download/dump route. '''
1165
1166 verbs = 'GET'
1167 rules = ['/download/dump/<dumpid>']
1168 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -05001169 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -04001170 suppress_json_resp = True
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001171 suppress_logging = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001172
1173 def __init__(self, app, bus):
1174 super(DownloadDumpHandler, self).__init__(
1175 app, bus, self.verbs, self.rules, self.content_type)
1176
1177 def do_get(self, dumpid):
1178 return self.do_download(dumpid)
1179
1180 def find(self, **kw):
1181 pass
1182
1183 def setup(self, **kw):
1184 pass
1185
1186 def do_download(self, dumpid):
1187 dump_loc = os.path.join(self.dump_loc, dumpid)
1188 if not os.path.exists(dump_loc):
1189 abort(404, "Path not found")
1190
1191 files = os.listdir(dump_loc)
1192 num_files = len(files)
1193 if num_files == 0:
1194 abort(404, "Dump not found")
1195
1196 return static_file(os.path.basename(files[0]), root=dump_loc,
1197 download=True, mimetype=self.content_type)
1198
1199
Matt Spinlerd41643e2018-02-02 13:51:38 -06001200class WebHandler(RouteHandler):
1201 ''' Handles the routes for the web UI files. '''
1202
1203 verbs = 'GET'
1204
1205 # Match only what we know are web files, so everything else
1206 # can get routed to the REST handlers.
1207 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1208 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1209 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1210 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1211 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1212 '/<filename:re:.+\.ico>']
1213
1214 # The mimetypes module knows about most types, but not these
1215 content_types = {
1216 '.eot': 'application/vnd.ms-fontobject',
1217 '.woff': 'application/x-font-woff',
1218 '.woff2': 'application/x-font-woff2',
1219 '.ttf': 'application/x-font-ttf',
1220 '.map': 'application/json'
1221 }
1222
1223 _require_auth = None
1224 suppress_json_resp = True
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001225 suppress_logging = True
Matt Spinlerd41643e2018-02-02 13:51:38 -06001226
1227 def __init__(self, app, bus):
1228 super(WebHandler, self).__init__(
1229 app, bus, self.verbs, self.rules)
1230
1231 def get_type(self, filename):
1232 ''' Returns the content type and encoding for a file '''
1233
1234 content_type, encoding = mimetypes.guess_type(filename)
1235
1236 # Try our own list if mimetypes didn't recognize it
1237 if content_type is None:
1238 if filename[-3:] == '.gz':
1239 filename = filename[:-3]
1240 extension = filename[filename.rfind('.'):]
1241 content_type = self.content_types.get(extension, None)
1242
1243 return content_type, encoding
1244
1245 def do_get(self, filename='index.html'):
1246
1247 # If a gzipped version exists, use that instead.
1248 # Possible future enhancement: if the client doesn't
1249 # accept compressed files, unzip it ourselves before sending.
1250 if not os.path.exists(os.path.join(www_base_path, filename)):
1251 filename = filename + '.gz'
1252
1253 # Though bottle should protect us, ensure path is valid
1254 realpath = os.path.realpath(filename)
1255 if realpath[0] == '/':
1256 realpath = realpath[1:]
1257 if not os.path.exists(os.path.join(www_base_path, realpath)):
1258 abort(404, "Path not found")
1259
1260 mimetype, encoding = self.get_type(filename)
1261
1262 # Couldn't find the type - let static_file() deal with it,
1263 # though this should never happen.
1264 if mimetype is None:
1265 print("Can't figure out content-type for %s" % filename)
1266 mimetype = 'auto'
1267
1268 # This call will set several header fields for us,
1269 # including the charset if the type is text.
1270 response = static_file(filename, www_base_path, mimetype)
1271
1272 # static_file() will only set the encoding if the
1273 # mimetype was auto, so set it here.
1274 if encoding is not None:
1275 response.set_header('Content-Encoding', encoding)
1276
1277 return response
1278
1279 def find(self, **kw):
1280 pass
1281
1282 def setup(self, **kw):
1283 pass
1284
1285
Brad Bishop2f428582015-12-02 10:56:11 -05001286class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001287 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -05001288
Brad Bishop87b63c12016-03-18 14:47:51 -04001289 name = 'authorization'
1290 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -05001291
Brad Bishop87b63c12016-03-18 14:47:51 -04001292 class Compose:
1293 def __init__(self, validators, callback, session_mgr):
1294 self.validators = validators
1295 self.callback = callback
1296 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -05001297
Brad Bishop87b63c12016-03-18 14:47:51 -04001298 def __call__(self, *a, **kw):
1299 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1300 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001301 if request.method != 'OPTIONS':
1302 for x in self.validators:
1303 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001304
Brad Bishop87b63c12016-03-18 14:47:51 -04001305 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001306
Brad Bishop87b63c12016-03-18 14:47:51 -04001307 def apply(self, callback, route):
1308 undecorated = route.get_undecorated_callback()
1309 if not isinstance(undecorated, RouteHandler):
1310 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001311
Brad Bishop87b63c12016-03-18 14:47:51 -04001312 auth_types = getattr(
1313 undecorated, '_require_auth', None)
1314 if not auth_types:
1315 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001316
Brad Bishop87b63c12016-03-18 14:47:51 -04001317 return self.Compose(
1318 auth_types, callback, undecorated.app.session_handler)
1319
Brad Bishop2f428582015-12-02 10:56:11 -05001320
Brad Bishopd0c404a2017-02-21 09:23:25 -05001321class CorsPlugin(object):
1322 ''' Add CORS headers. '''
1323
1324 name = 'cors'
1325 api = 2
1326
1327 @staticmethod
1328 def process_origin():
1329 origin = request.headers.get('Origin')
1330 if origin:
1331 response.add_header('Access-Control-Allow-Origin', origin)
1332 response.add_header(
1333 'Access-Control-Allow-Credentials', 'true')
1334
1335 @staticmethod
1336 def process_method_and_headers(verbs):
1337 method = request.headers.get('Access-Control-Request-Method')
1338 headers = request.headers.get('Access-Control-Request-Headers')
1339 if headers:
1340 headers = [x.lower() for x in headers.split(',')]
1341
1342 if method in verbs \
1343 and headers == ['content-type']:
1344 response.add_header('Access-Control-Allow-Methods', method)
1345 response.add_header(
1346 'Access-Control-Allow-Headers', 'Content-Type')
Ratan Gupta91b46f82018-01-14 12:52:23 +05301347 response.add_header('X-Frame-Options', 'deny')
1348 response.add_header('X-Content-Type-Options', 'nosniff')
1349 response.add_header('X-XSS-Protection', '1; mode=block')
1350 response.add_header(
1351 'Content-Security-Policy', "default-src 'self'")
1352 response.add_header(
1353 'Strict-Transport-Security',
1354 'max-age=31536000; includeSubDomains; preload')
Brad Bishopd0c404a2017-02-21 09:23:25 -05001355
1356 def __init__(self, app):
1357 app.install_error_callback(self.error_callback)
1358
1359 def apply(self, callback, route):
1360 undecorated = route.get_undecorated_callback()
1361 if not isinstance(undecorated, RouteHandler):
1362 return callback
1363
1364 if not getattr(undecorated, '_enable_cors', None):
1365 return callback
1366
1367 def wrap(*a, **kw):
1368 self.process_origin()
1369 self.process_method_and_headers(undecorated._verbs)
1370 return callback(*a, **kw)
1371
1372 return wrap
1373
1374 def error_callback(self, **kw):
1375 self.process_origin()
1376
1377
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001378class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001379 ''' Ensures request content satisfies the OpenBMC json api format. '''
1380 name = 'json_api_request'
1381 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001382
Brad Bishop87b63c12016-03-18 14:47:51 -04001383 error_str = "Expecting request format { 'data': <value> }, got '%s'"
1384 type_error_str = "Unsupported Content-Type: '%s'"
1385 json_type = "application/json"
1386 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001387
Brad Bishop87b63c12016-03-18 14:47:51 -04001388 @staticmethod
1389 def content_expected():
1390 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001391
Brad Bishop87b63c12016-03-18 14:47:51 -04001392 def validate_request(self):
1393 if request.content_length > 0 and \
1394 request.content_type != self.json_type:
1395 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001396
Brad Bishop87b63c12016-03-18 14:47:51 -04001397 try:
1398 request.parameter_list = request.json.get('data')
CamVan Nguyen249d1322018-03-05 10:08:33 -06001399 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -04001400 abort(400, str(e))
1401 except (AttributeError, KeyError, TypeError):
1402 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001403
Brad Bishop87b63c12016-03-18 14:47:51 -04001404 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001405 content_type = getattr(
1406 route.get_undecorated_callback(), '_content_type', None)
1407 if self.json_type != content_type:
1408 return callback
1409
Brad Bishop87b63c12016-03-18 14:47:51 -04001410 verbs = getattr(
1411 route.get_undecorated_callback(), '_verbs', None)
1412 if verbs is None:
1413 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001414
Brad Bishop87b63c12016-03-18 14:47:51 -04001415 if not set(self.request_methods).intersection(verbs):
1416 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001417
Brad Bishop87b63c12016-03-18 14:47:51 -04001418 def wrap(*a, **kw):
1419 if self.content_expected():
1420 self.validate_request()
1421 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001422
Brad Bishop87b63c12016-03-18 14:47:51 -04001423 return wrap
1424
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001425
1426class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001427 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1428 name = 'json_api_method_request'
1429 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001430
Brad Bishop87b63c12016-03-18 14:47:51 -04001431 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001432 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001433
Brad Bishop87b63c12016-03-18 14:47:51 -04001434 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001435 content_type = getattr(
1436 route.get_undecorated_callback(), '_content_type', None)
1437 if self.json_type != content_type:
1438 return callback
1439
Brad Bishop87b63c12016-03-18 14:47:51 -04001440 request_type = getattr(
1441 route.get_undecorated_callback(), 'request_type', None)
1442 if request_type is None:
1443 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001444
Brad Bishop87b63c12016-03-18 14:47:51 -04001445 def validate_request():
1446 if not isinstance(request.parameter_list, request_type):
1447 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001448
Brad Bishop87b63c12016-03-18 14:47:51 -04001449 def wrap(*a, **kw):
1450 if JsonApiRequestPlugin.content_expected():
1451 validate_request()
1452 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001453
Brad Bishop87b63c12016-03-18 14:47:51 -04001454 return wrap
1455
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001456
Brad Bishop080a48e2017-02-21 22:34:43 -05001457class JsonErrorsPlugin(JSONPlugin):
1458 ''' Extend the Bottle JSONPlugin such that it also encodes error
1459 responses. '''
1460
1461 def __init__(self, app, **kw):
1462 super(JsonErrorsPlugin, self).__init__(**kw)
1463 self.json_opts = {
CamVan Nguyen249d1322018-03-05 10:08:33 -06001464 x: y for x, y in kw.items()
Brad Bishop080a48e2017-02-21 22:34:43 -05001465 if x in ['indent', 'sort_keys']}
1466 app.install_error_callback(self.error_callback)
1467
1468 def error_callback(self, response_object, response_body, **kw):
1469 response_body['body'] = json.dumps(response_object, **self.json_opts)
1470 response.content_type = 'application/json'
1471
1472
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001473class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001474 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001475 name = 'json_api_response'
1476 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001477
Brad Bishopd4c1c552017-02-21 00:07:28 -05001478 @staticmethod
1479 def has_body():
1480 return request.method not in ['OPTIONS']
1481
Brad Bishop080a48e2017-02-21 22:34:43 -05001482 def __init__(self, app):
1483 app.install_error_callback(self.error_callback)
1484
Matt Spinler6691e7c2018-06-25 14:11:58 -05001485 @staticmethod
1486 def dbus_boolean_to_bool(data):
1487 ''' Convert all dbus.Booleans to true/false instead of 1/0 as
1488 the JSON encoder thinks they're ints. Note that unlike
1489 dicts and lists, tuples (from a dbus.Struct) are immutable
1490 so they need special handling. '''
1491
1492 def walkdict(data):
1493 for key, value in data.items():
1494 if isinstance(value, dbus.Boolean):
1495 data[key] = bool(value)
1496 elif isinstance(value, tuple):
1497 data[key] = walktuple(value)
1498 else:
1499 JsonApiResponsePlugin.dbus_boolean_to_bool(value)
1500
1501 def walklist(data):
1502 for i in range(len(data)):
1503 if isinstance(data[i], dbus.Boolean):
1504 data[i] = bool(data[i])
1505 elif isinstance(data[i], tuple):
1506 data[i] = walktuple(data[i])
1507 else:
1508 JsonApiResponsePlugin.dbus_boolean_to_bool(data[i])
1509
1510 def walktuple(data):
1511 new = []
1512 for item in data:
1513 if isinstance(item, dbus.Boolean):
1514 item = bool(item)
1515 else:
1516 JsonApiResponsePlugin.dbus_boolean_to_bool(item)
1517 new.append(item)
1518 return tuple(new)
1519
1520 if isinstance(data, dict):
1521 walkdict(data)
1522 elif isinstance(data, list):
1523 walklist(data)
1524
Brad Bishop87b63c12016-03-18 14:47:51 -04001525 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001526 skip = getattr(
1527 route.get_undecorated_callback(), 'suppress_json_resp', None)
1528 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001529 return callback
1530
Brad Bishop87b63c12016-03-18 14:47:51 -04001531 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001532 data = callback(*a, **kw)
Matt Spinler6691e7c2018-06-25 14:11:58 -05001533 JsonApiResponsePlugin.dbus_boolean_to_bool(data)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001534 if self.has_body():
1535 resp = {'data': data}
1536 resp['status'] = 'ok'
1537 resp['message'] = response.status_line
1538 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001539 return wrap
1540
Brad Bishop080a48e2017-02-21 22:34:43 -05001541 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001542 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001543 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001544 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001545 if error.status_code == 500:
1546 response_object['data']['exception'] = repr(error.exception)
1547 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001548
Brad Bishop87b63c12016-03-18 14:47:51 -04001549
Brad Bishop080a48e2017-02-21 22:34:43 -05001550class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001551 ''' Json javascript wrapper. '''
1552 name = 'jsonp'
1553 api = 2
1554
Brad Bishop080a48e2017-02-21 22:34:43 -05001555 def __init__(self, app, **kw):
1556 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001557
1558 @staticmethod
1559 def to_jsonp(json):
1560 jwrapper = request.query.callback or None
1561 if(jwrapper):
1562 response.set_header('Content-Type', 'application/javascript')
1563 json = jwrapper + '(' + json + ');'
1564 return json
1565
1566 def apply(self, callback, route):
1567 def wrap(*a, **kw):
1568 return self.to_jsonp(callback(*a, **kw))
1569 return wrap
1570
Brad Bishop080a48e2017-02-21 22:34:43 -05001571 def error_callback(self, response_body, **kw):
1572 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001573
1574
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001575class ContentCheckerPlugin(object):
1576 ''' Ensures that a route is associated with the expected content-type
1577 header. '''
1578 name = 'content_checker'
1579 api = 2
1580
1581 class Checker:
1582 def __init__(self, type, callback):
1583 self.expected_type = type
1584 self.callback = callback
1585 self.error_str = "Expecting content type '%s', got '%s'"
1586
1587 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001588 if request.method in ['PUT', 'POST', 'PATCH'] and \
1589 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001590 self.expected_type != request.content_type:
1591 abort(415, self.error_str % (self.expected_type,
1592 request.content_type))
1593
1594 return self.callback(*a, **kw)
1595
1596 def apply(self, callback, route):
1597 content_type = getattr(
1598 route.get_undecorated_callback(), '_content_type', None)
1599
1600 return self.Checker(content_type, callback)
1601
1602
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001603class LoggingPlugin(object):
1604 ''' Wraps a request in order to emit a log after the request is handled. '''
1605 name = 'loggingp'
1606 api = 2
1607
1608 class Logger:
1609 def __init__(self, suppress_json_logging, callback, app):
1610 self.suppress_json_logging = suppress_json_logging
1611 self.callback = callback
1612 self.app = app
Deepak Kodihalli95803682018-09-07 03:13:59 -05001613 self.logging_enabled = None
1614 self.bus = dbus.SystemBus()
1615 self.dbus_path = '/xyz/openbmc_project/logging/rest_api_logs'
Deepak Kodihalli4b412ac2018-10-15 12:45:18 -05001616 self.no_json = [
1617 '/xyz/openbmc_project/user/ldap/action/CreateConfig'
1618 ]
Deepak Kodihalli95803682018-09-07 03:13:59 -05001619 self.bus.add_signal_receiver(
1620 self.properties_changed_handler,
1621 dbus_interface=dbus.PROPERTIES_IFACE,
1622 signal_name='PropertiesChanged',
1623 path=self.dbus_path)
1624 Greenlet.spawn(self.dbus_loop)
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001625
1626 def __call__(self, *a, **kw):
1627 resp = self.callback(*a, **kw)
Deepak Kodihalli95803682018-09-07 03:13:59 -05001628 if not self.enabled():
Deepak Kodihalli4aa10002018-09-13 11:48:45 -05001629 return resp
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001630 if request.method == 'GET':
Deepak Kodihalli4aa10002018-09-13 11:48:45 -05001631 return resp
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001632 json = request.json
1633 if self.suppress_json_logging:
1634 json = None
Deepak Kodihalli4b412ac2018-10-15 12:45:18 -05001635 elif any(substring in request.url for substring in self.no_json):
1636 json = None
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001637 session = self.app.session_handler.get_session_from_cookie()
1638 user = None
1639 if "/login" in request.url:
1640 user = request.parameter_list[0]
1641 elif session is not None:
1642 user = session['user']
1643 print("{remote} user:{user} {method} {url} json:{json} {status}" \
1644 .format(
1645 user=user,
1646 remote=request.remote_addr,
1647 method=request.method,
1648 url=request.url,
1649 json=json,
1650 status=response.status))
Deepak Kodihalli4aa10002018-09-13 11:48:45 -05001651 return resp
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001652
Deepak Kodihalli95803682018-09-07 03:13:59 -05001653 def enabled(self):
1654 if self.logging_enabled is None:
1655 try:
1656 obj = self.bus.get_object(
1657 'xyz.openbmc_project.Settings',
1658 self.dbus_path)
1659 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
1660 logging_enabled = iface.Get(
1661 'xyz.openbmc_project.Object.Enable',
1662 'Enabled')
1663 self.logging_enabled = logging_enabled
1664 except dbus.exceptions.DBusException:
1665 self.logging_enabled = False
1666 return self.logging_enabled
1667
1668 def dbus_loop(self):
1669 loop = gobject.MainLoop()
1670 gcontext = loop.get_context()
1671 while loop is not None:
1672 try:
1673 if gcontext.pending():
1674 gcontext.iteration()
1675 else:
1676 gevent.sleep(5)
1677 except Exception as e:
1678 break
1679
1680 def properties_changed_handler(self, interface, new, old, **kw):
1681 self.logging_enabled = new.values()[0]
1682
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001683 def apply(self, callback, route):
1684 cb = route.get_undecorated_callback()
1685 skip = getattr(
1686 cb, 'suppress_logging', None)
1687 if skip:
1688 return callback
1689
1690 suppress_json_logging = getattr(
1691 cb, 'suppress_json_logging', None)
1692 return self.Logger(suppress_json_logging, callback, cb.app)
1693
1694
Brad Bishop2c6fc762016-08-29 15:53:25 -04001695class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001696 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001697 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001698
1699 self.have_wsock = kw.get('have_wsock', False)
Alexander Filippovd08a4562018-03-20 12:02:23 +03001700 self.with_bmc_check = '--with-bmc-check' in sys.argv
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001701
Brad Bishop2ddfa002016-08-29 15:11:55 -04001702 self.bus = dbus.SystemBus()
1703 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001704 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001705
Brad Bishop87b63c12016-03-18 14:47:51 -04001706 self.install_hooks()
1707 self.install_plugins()
1708 self.create_handlers()
1709 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001710
Brad Bishop87b63c12016-03-18 14:47:51 -04001711 def install_plugins(self):
1712 # install json api plugins
1713 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001714 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001715 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001716 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001717 self.install(JsonpPlugin(self, **json_kw))
1718 self.install(JsonErrorsPlugin(self, **json_kw))
1719 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001720 self.install(JsonApiRequestPlugin())
1721 self.install(JsonApiRequestTypePlugin())
Deepak Kodihalli6e1ca532018-09-04 03:51:04 -05001722 self.install(LoggingPlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001723
Brad Bishop87b63c12016-03-18 14:47:51 -04001724 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001725 self.error_handler_type = type(self.default_error_handler)
1726 self.original_error_handler = self.default_error_handler
1727 self.default_error_handler = self.error_handler_type(
1728 self.custom_error_handler, self, Bottle)
1729
Brad Bishop87b63c12016-03-18 14:47:51 -04001730 self.real_router_match = self.router.match
1731 self.router.match = self.custom_router_match
1732 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001733
Brad Bishop87b63c12016-03-18 14:47:51 -04001734 def create_handlers(self):
1735 # create route handlers
1736 self.session_handler = SessionHandler(self, self.bus)
Matt Spinlerd41643e2018-02-02 13:51:38 -06001737 self.web_handler = WebHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001738 self.directory_handler = DirectoryHandler(self, self.bus)
1739 self.list_names_handler = ListNamesHandler(self, self.bus)
1740 self.list_handler = ListHandler(self, self.bus)
1741 self.method_handler = MethodHandler(self, self.bus)
1742 self.property_handler = PropertyHandler(self, self.bus)
1743 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001744 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1745 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001746 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -05001747 self.certificate_put_handler = CertificatePutHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001748 if self.have_wsock:
1749 self.event_handler = EventHandler(self, self.bus)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001750 self.host_console_handler = HostConsoleHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001751 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001752
Brad Bishop87b63c12016-03-18 14:47:51 -04001753 def install_handlers(self):
1754 self.session_handler.install()
Matt Spinlerd41643e2018-02-02 13:51:38 -06001755 self.web_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001756 self.directory_handler.install()
1757 self.list_names_handler.install()
1758 self.list_handler.install()
1759 self.method_handler.install()
1760 self.property_handler.install()
1761 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001762 self.image_upload_post_handler.install()
1763 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001764 self.download_dump_get_handler.install()
Dhruvaraj Subhashchandrandee2ef52018-09-05 05:36:31 -05001765 self.certificate_put_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001766 if self.have_wsock:
1767 self.event_handler.install()
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001768 self.host_console_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001769 # this has to come last, since it matches everything
1770 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001771
Brad Bishop080a48e2017-02-21 22:34:43 -05001772 def install_error_callback(self, callback):
1773 self.error_callbacks.insert(0, callback)
1774
Brad Bishop87b63c12016-03-18 14:47:51 -04001775 def custom_router_match(self, environ):
1776 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1777 needed doesn't work for us since the instance rules match
1778 everything. This monkey-patch lets the route handler figure
1779 out which response is needed. This could be accomplished
1780 with a hook but that would require calling the router match
1781 function twice.
1782 '''
1783 route, args = self.real_router_match(environ)
1784 if isinstance(route.callback, RouteHandler):
1785 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001786
Brad Bishop87b63c12016-03-18 14:47:51 -04001787 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001788
Brad Bishop080a48e2017-02-21 22:34:43 -05001789 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001790 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001791 error handler. '''
1792
1793 response_object = {}
1794 response_body = {}
1795 for x in self.error_callbacks:
1796 x(error=error,
1797 response_object=response_object,
1798 response_body=response_body)
1799
1800 return response_body.get('body', "")
1801
Brad Bishop87b63c12016-03-18 14:47:51 -04001802 @staticmethod
1803 def strip_extra_slashes():
1804 path = request.environ['PATH_INFO']
1805 trailing = ("", "/")[path[-1] == '/']
CamVan Nguyen249d1322018-03-05 10:08:33 -06001806 parts = list(filter(bool, path.split('/')))
Brad Bishop87b63c12016-03-18 14:47:51 -04001807 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing