blob: ffbab946bf470793b537d91f156b3cc96c2ec542 [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
Jayanth Othayoth9bc94992017-06-29 06:30:40 -050023from bottle import static_file
Brad Bishopb103d2d2016-03-04 16:19:14 -050024import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050025from obmc.dbuslib.introspection import IntrospectionNodeParser
26import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050027import spwd
28import grp
29import crypt
Deepak Kodihalli1af301a2017-04-11 07:29:01 -050030import tempfile
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050031import re
Matt Spinlerd41643e2018-02-02 13:51:38 -060032import mimetypes
Deepak Kodihalli639b5022017-10-13 06:40:26 -050033have_wsock = True
34try:
35 from geventwebsocket import WebSocketError
36except ImportError:
37 have_wsock = False
38if have_wsock:
39 from dbus.mainloop.glib import DBusGMainLoop
40 DBusGMainLoop(set_as_default=True)
CamVan Nguyen249d1322018-03-05 10:08:33 -060041 # TODO: openbmc/openbmc#2994 remove python 2 support
42 try: # python 2
43 import gobject
44 except ImportError: # python 3
45 from gi.repository import GObject as gobject
Deepak Kodihalli639b5022017-10-13 06:40:26 -050046 import gevent
Deepak Kodihalli5c518f62018-04-23 03:26:38 -050047 from gevent import socket
48 from gevent import Greenlet
Brad Bishopaa65f6e2015-10-27 16:28:51 -040049
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -060050DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050051DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
52DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050053DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Deepak Kodihalli6075bb42017-04-04 05:49:17 -050054DELETE_IFACE = 'xyz.openbmc_project.Object.Delete'
Adriana Kobylak53693892018-03-12 13:05:50 -050055SOFTWARE_PATH = '/xyz/openbmc_project/software'
Brad Bishop9ee57c42015-11-03 14:59:29 -050056
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050057_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040058
Matt Spinlerd41643e2018-02-02 13:51:38 -060059www_base_path = '/usr/share/www/'
60
Brad Bishop87b63c12016-03-18 14:47:51 -040061
Brad Bishop2f428582015-12-02 10:56:11 -050062def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040063 ''' Authorization plugin callback that checks
64 that the user is logged in. '''
65 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040066 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040067
Brad Bishop2f428582015-12-02 10:56:11 -050068
Leonel Gonzalez0bdef952017-04-18 08:17:49 -050069def get_type_signature_by_introspection(bus, service, object_path,
70 property_name):
71 obj = bus.get_object(service, object_path)
72 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
73 xml_string = iface.Introspect()
74 for child in ElementTree.fromstring(xml_string):
75 # Iterate over each interfaces's properties to find
76 # matching property_name, and return its signature string
77 if child.tag == 'interface':
78 for i in child.iter():
79 if ('name' in i.attrib) and \
80 (i.attrib['name'] == property_name):
81 type_signature = i.attrib['type']
82 return type_signature
83
84
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +053085def get_method_signature(bus, service, object_path, interface, method):
86 obj = bus.get_object(service, object_path)
87 iface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
88 xml_string = iface.Introspect()
89 arglist = []
90
91 root = ElementTree.fromstring(xml_string)
92 for dbus_intf in root.findall('interface'):
93 if (dbus_intf.get('name') == interface):
94 for dbus_method in dbus_intf.findall('method'):
95 if(dbus_method.get('name') == method):
96 for arg in dbus_method.findall('arg'):
97 arglist.append(arg.get('type'))
98 return arglist
99
100
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500101def split_struct_signature(signature):
102 struct_regex = r'(b|y|n|i|x|q|u|t|d|s|a\(.+?\)|\(.+?\))|a\{.+?\}+?'
103 struct_matches = re.findall(struct_regex, signature)
104 return struct_matches
105
106
107def convert_type(signature, value):
108 # Basic Types
109 converted_value = None
110 converted_container = None
CamVan Nguyen249d1322018-03-05 10:08:33 -0600111 # TODO: openbmc/openbmc#2994 remove python 2 support
112 try: # python 2
113 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
114 'x': long, 'q': dbus.UInt16, 'u': dbus.UInt32,
115 't': dbus.UInt64, 'd': float, 's': str}
116 except NameError: # python 3
117 basic_types = {'b': bool, 'y': dbus.Byte, 'n': dbus.Int16, 'i': int,
118 'x': int, 'q': dbus.UInt16, 'u': dbus.UInt32,
119 't': dbus.UInt64, 'd': float, 's': str}
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500120 array_matches = re.match(r'a\((\S+)\)', signature)
121 struct_matches = re.match(r'\((\S+)\)', signature)
122 dictionary_matches = re.match(r'a{(\S+)}', signature)
123 if signature in basic_types:
124 converted_value = basic_types[signature](value)
125 return converted_value
126 # Array
127 if array_matches:
128 element_type = array_matches.group(1)
129 converted_container = list()
130 # Test if value is a list
131 # to avoid iterating over each character in a string.
132 # Iterate over each item and convert type
133 if isinstance(value, list):
134 for i in value:
135 converted_element = convert_type(element_type, i)
136 converted_container.append(converted_element)
137 # Convert non-sequence to expected type, and append to list
138 else:
139 converted_element = convert_type(element_type, value)
140 converted_container.append(converted_element)
141 return converted_container
142 # Struct
143 if struct_matches:
144 element_types = struct_matches.group(1)
145 split_element_types = split_struct_signature(element_types)
146 converted_container = list()
147 # Test if value is a list
148 if isinstance(value, list):
149 for index, val in enumerate(value):
150 converted_element = convert_type(split_element_types[index],
151 value[index])
152 converted_container.append(converted_element)
153 else:
154 converted_element = convert_type(element_types, value)
155 converted_container.append(converted_element)
156 return tuple(converted_container)
157 # Dictionary
158 if dictionary_matches:
159 element_types = dictionary_matches.group(1)
160 split_element_types = split_struct_signature(element_types)
161 converted_container = dict()
162 # Convert each element of dict
CamVan Nguyen249d1322018-03-05 10:08:33 -0600163 for key, val in value.items():
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500164 converted_key = convert_type(split_element_types[0], key)
165 converted_val = convert_type(split_element_types[1], val)
166 converted_container[converted_key] = converted_val
167 return converted_container
168
169
Brad Bishop2f428582015-12-02 10:56:11 -0500170class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -0400171 ''' Authorization plugin callback that checks that the user is logged in
172 and a member of a group. '''
173 def __init__(self, group):
174 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -0500175
Brad Bishop87b63c12016-03-18 14:47:51 -0400176 def __call__(self, session, *a, **kw):
177 valid_user(session, *a, **kw)
178 res = False
Brad Bishop2f428582015-12-02 10:56:11 -0500179
Brad Bishop87b63c12016-03-18 14:47:51 -0400180 try:
181 res = session['user'] in grp.getgrnam(self.group)[3]
182 except KeyError:
183 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500184
Brad Bishop87b63c12016-03-18 14:47:51 -0400185 if not res:
186 abort(403, 'Insufficient access')
187
Brad Bishop2f428582015-12-02 10:56:11 -0500188
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500189class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -0400190 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopd0c404a2017-02-21 09:23:25 -0500191 _enable_cors = True
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400192
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500193 def __init__(self, app, bus, verbs, rules, content_type=''):
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 self.app = app
195 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500196 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -0400197 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -0400198 self._rules = rules
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500199 self._content_type = content_type
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400200
Brad Bishop88c76a42017-02-21 00:02:02 -0500201 if 'GET' in self._verbs:
202 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -0500203 if 'OPTIONS' not in self._verbs:
204 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -0500205
Brad Bishop87b63c12016-03-18 14:47:51 -0400206 def _setup(self, **kw):
207 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -0500208
Brad Bishop87b63c12016-03-18 14:47:51 -0400209 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -0500210 if request.method != 'OPTIONS':
211 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -0500212
Brad Bishopd4c1c552017-02-21 00:07:28 -0500213 # Javascript implementations will not send credentials
214 # with an OPTIONS request. Don't help malicious clients
215 # by checking the path here and returning a 404 if the
216 # path doesn't exist.
217 return None
Brad Bishop88c76a42017-02-21 00:02:02 -0500218
Brad Bishopd4c1c552017-02-21 00:07:28 -0500219 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -0500220 raise HTTPError(
221 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400222
Brad Bishop87b63c12016-03-18 14:47:51 -0400223 def __call__(self, **kw):
224 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400225
Brad Bishop88c76a42017-02-21 00:02:02 -0500226 def do_head(self, **kw):
227 return self.do_get(**kw)
228
Brad Bishopd4c1c552017-02-21 00:07:28 -0500229 def do_options(self, **kw):
230 for v in self._verbs:
231 response.set_header(
232 'Allow',
233 ','.join(self._verbs))
234 return None
235
Brad Bishop87b63c12016-03-18 14:47:51 -0400236 def install(self):
237 self.app.route(
238 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500239 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400240
Brad Bishop87b63c12016-03-18 14:47:51 -0400241 @staticmethod
242 def try_mapper_call(f, callback=None, **kw):
243 try:
244 return f(**kw)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600245 except dbus.exceptions.DBusException as e:
Brad Bishopfce77562016-11-28 15:44:18 -0500246 if e.get_dbus_name() == \
247 'org.freedesktop.DBus.Error.ObjectPathInUse':
248 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500249 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400250 raise
251 if callback is None:
252 def callback(e, **kw):
253 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400254
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 callback(e, **kw)
256
257 @staticmethod
258 def try_properties_interface(f, *a):
259 try:
260 return f(*a)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600261 except dbus.exceptions.DBusException as e:
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -0600262 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_name():
Brad Bishopf4e74982016-04-01 14:53:05 -0400263 # interface doesn't have any properties
264 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400265 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
266 # properties interface not implemented at all
267 return None
268 raise
269
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400270
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500271class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400272 verbs = 'GET'
273 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400274
Brad Bishop87b63c12016-03-18 14:47:51 -0400275 def __init__(self, app, bus):
276 super(DirectoryHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400277 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def find(self, path='/'):
280 return self.try_mapper_call(
281 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400282
Brad Bishop87b63c12016-03-18 14:47:51 -0400283 def setup(self, path='/'):
284 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 def do_get(self, path='/'):
287 return request.route_data['map']
288
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500290class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400291 verbs = 'GET'
292 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400293
Brad Bishop87b63c12016-03-18 14:47:51 -0400294 def __init__(self, app, bus):
295 super(ListNamesHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400296 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400297
Brad Bishop87b63c12016-03-18 14:47:51 -0400298 def find(self, path='/'):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600299 return list(self.try_mapper_call(
300 self.mapper.get_subtree, path=path).keys())
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400301
Brad Bishop87b63c12016-03-18 14:47:51 -0400302 def setup(self, path='/'):
303 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400304
Brad Bishop87b63c12016-03-18 14:47:51 -0400305 def do_get(self, path='/'):
306 return request.route_data['map']
307
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400308
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500309class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 verbs = 'GET'
311 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400312
Brad Bishop87b63c12016-03-18 14:47:51 -0400313 def __init__(self, app, bus):
314 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400315 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400316
Brad Bishop87b63c12016-03-18 14:47:51 -0400317 def find(self, path='/'):
318 return self.try_mapper_call(
319 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400320
Brad Bishop87b63c12016-03-18 14:47:51 -0400321 def setup(self, path='/'):
322 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400323
Brad Bishop87b63c12016-03-18 14:47:51 -0400324 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400325 return {x: y for x, y in self.mapper.enumerate_subtree(
326 path,
327 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400328
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400329
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500330class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 verbs = 'POST'
332 rules = '<path:path>/action/<method>'
333 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500334 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400335
Brad Bishop87b63c12016-03-18 14:47:51 -0400336 def __init__(self, app, bus):
337 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500338 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530339 self.service = ''
340 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400341
Brad Bishop87b63c12016-03-18 14:47:51 -0400342 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500343 method_list = []
Gunnar Mills313aadb2018-04-08 14:50:09 -0500344 buses = self.try_mapper_call(
Brad Bishop87b63c12016-03-18 14:47:51 -0400345 self.mapper.get_object, path=path)
Gunnar Mills313aadb2018-04-08 14:50:09 -0500346 for items in buses.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400347 m = self.find_method_on_bus(path, method, *items)
348 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500349 method_list.append(m)
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600350 if method_list:
351 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400352
Brad Bishop87b63c12016-03-18 14:47:51 -0400353 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400354
Brad Bishop87b63c12016-03-18 14:47:51 -0400355 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500356 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400357
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600358 def do_post(self, path, method, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400359 try:
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600360 args = []
361 if request.parameter_list:
362 args = request.parameter_list
363 # To see if the return type is capable of being merged
364 if len(request.route_data['map']) > 1:
365 results = None
366 for item in request.route_data['map']:
367 tmp = item(*args)
368 if not results:
369 if tmp is not None:
370 results = type(tmp)()
371 if isinstance(results, dict):
372 results = results.update(tmp)
373 elif isinstance(results, list):
374 results = results + tmp
375 elif isinstance(results, type(None)):
376 results = None
377 else:
378 abort(501, 'Don\'t know how to merge method call '
379 'results of {}'.format(type(tmp)))
380 return results
381 # There is only one method
382 return request.route_data['map'][0](*args)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400383
CamVan Nguyen249d1322018-03-05 10:08:33 -0600384 except dbus.exceptions.DBusException as e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530385 paramlist = []
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500386 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530387
388 signature_list = get_method_signature(self.bus, self.service,
389 path, self.interface,
390 method)
391 if not signature_list:
392 abort(400, "Failed to get method signature: %s" % str(e))
393 if len(signature_list) != len(request.parameter_list):
394 abort(400, "Invalid number of args")
395 converted_value = None
396 try:
397 for index, expected_type in enumerate(signature_list):
398 value = request.parameter_list[index]
399 converted_value = convert_type(expected_type, value)
400 paramlist.append(converted_value)
401 request.parameter_list = paramlist
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600402 self.do_post(path, method, False)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530403 return
404 except Exception as ex:
Nagaraju Gorugantiab404fa2017-12-14 10:24:40 -0600405 abort(400, "Bad Request/Invalid Args given")
Brad Bishop87b63c12016-03-18 14:47:51 -0400406 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530407
Brad Bishop87b63c12016-03-18 14:47:51 -0400408 if e.get_dbus_name() == DBUS_TYPE_ERROR:
409 abort(400, str(e))
410 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400411
Brad Bishop87b63c12016-03-18 14:47:51 -0400412 @staticmethod
413 def find_method_in_interface(method, obj, interface, methods):
414 if methods is None:
415 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400416
CamVan Nguyen249d1322018-03-05 10:08:33 -0600417 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
Brad Bishop87b63c12016-03-18 14:47:51 -0400418 if method is not None:
419 iface = dbus.Interface(obj, interface)
420 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400421
Brad Bishop87b63c12016-03-18 14:47:51 -0400422 def find_method_on_bus(self, path, method, bus, interfaces):
423 obj = self.bus.get_object(bus, path, introspect=False)
424 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
425 data = iface.Introspect()
426 parser = IntrospectionNodeParser(
427 ElementTree.fromstring(data),
Brad Bishopaeb995d2018-04-04 22:28:42 -0400428 intf_match=lambda x: x in interfaces)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600429 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 m = self.find_method_in_interface(
431 method, obj, x, y.get('method'))
432 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530433 self.service = bus
434 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400435 return m
436
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400437
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500438class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400439 verbs = ['PUT', 'GET']
440 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500441 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400442
Brad Bishop87b63c12016-03-18 14:47:51 -0400443 def __init__(self, app, bus):
444 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500445 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400446
Brad Bishop87b63c12016-03-18 14:47:51 -0400447 def find(self, path, prop):
448 self.app.instance_handler.setup(path)
449 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500450 real_name = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600451 prop, list(obj.keys()))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400452
Brad Bishop56ad87f2017-02-21 23:33:29 -0500453 if not real_name:
454 if request.method == 'PUT':
455 abort(403, _4034_msg % ('property', 'created', prop))
456 else:
457 abort(404, _4034_msg % ('property', 'found', prop))
458 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500459
Brad Bishop87b63c12016-03-18 14:47:51 -0400460 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500461 name, obj = self.find(path, prop)
462 request.route_data['obj'] = obj
463 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500466 name = request.route_data['name']
467 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500468
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600469 def do_put(self, path, prop, value=None, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 if value is None:
471 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500472
Brad Bishop87b63c12016-03-18 14:47:51 -0400473 prop, iface, properties_iface = self.get_host_interface(
474 path, prop, request.route_data['map'][path])
475 try:
476 properties_iface.Set(iface, prop, value)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600477 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -0400478 abort(400, str(e))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600479 except dbus.exceptions.DBusException as e:
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500480 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500481 bus_name = properties_iface.bus_name
482 expected_type = get_type_signature_by_introspection(self.bus,
483 bus_name,
484 path,
485 prop)
486 if not expected_type:
487 abort(403, "Failed to get expected type: %s" % str(e))
488 converted_value = None
489 try:
490 converted_value = convert_type(expected_type, value)
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600491 self.do_put(path, prop, converted_value, False)
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500492 return
493 except Exception as ex:
494 abort(403, "Failed to convert %s to type %s" %
495 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400496 abort(403, str(e))
497 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500498
Brad Bishop87b63c12016-03-18 14:47:51 -0400499 def get_host_interface(self, path, prop, bus_info):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600500 for bus, interfaces in bus_info.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400501 obj = self.bus.get_object(bus, path, introspect=True)
502 properties_iface = dbus.Interface(
503 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500504
Brad Bishop87b63c12016-03-18 14:47:51 -0400505 info = self.get_host_interface_on_bus(
506 path, prop, properties_iface, bus, interfaces)
507 if info is not None:
508 prop, iface = info
509 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500510
Brad Bishop87b63c12016-03-18 14:47:51 -0400511 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
512 for i in interfaces:
513 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500514 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500516 match = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600517 prop, list(properties.keys()))
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500518 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400519 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500520 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400521 return prop, i
522
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500523
Brad Bishop2503bd62015-12-16 17:56:12 -0500524class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 verbs = ['GET']
526 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500527
Brad Bishop87b63c12016-03-18 14:47:51 -0400528 def __init__(self, app, bus):
529 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400530 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500531
Brad Bishop87b63c12016-03-18 14:47:51 -0400532 def find(self, path):
533 return self.try_mapper_call(
534 self.mapper.get_object,
535 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500536
Brad Bishop87b63c12016-03-18 14:47:51 -0400537 def setup(self, path):
538 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500539
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 def do_get(self, path):
541 schema = {}
CamVan Nguyen249d1322018-03-05 10:08:33 -0600542 for x in request.route_data['map'].keys():
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 obj = self.bus.get_object(x, path, introspect=False)
544 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
545 data = iface.Introspect()
546 parser = IntrospectionNodeParser(
547 ElementTree.fromstring(data))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600548 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400549 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500550
Brad Bishop87b63c12016-03-18 14:47:51 -0400551 return schema
552
Brad Bishop2503bd62015-12-16 17:56:12 -0500553
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500554class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400555 verbs = ['GET', 'PUT', 'DELETE']
556 rules = '<path:path>'
557 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 def __init__(self, app, bus):
560 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400561 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500562
Brad Bishop87b63c12016-03-18 14:47:51 -0400563 def find(self, path, callback=None):
564 return {path: self.try_mapper_call(
565 self.mapper.get_object,
566 callback,
567 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500568
Brad Bishop87b63c12016-03-18 14:47:51 -0400569 def setup(self, path):
570 callback = None
571 if request.method == 'PUT':
572 def callback(e, **kw):
573 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500574
Brad Bishop87b63c12016-03-18 14:47:51 -0400575 if request.route_data.get('map') is None:
576 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500577
Brad Bishop87b63c12016-03-18 14:47:51 -0400578 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400579 return self.mapper.enumerate_object(
580 path,
581 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500582
Brad Bishop87b63c12016-03-18 14:47:51 -0400583 def do_put(self, path):
584 # make sure all properties exist in the request
585 obj = set(self.do_get(path).keys())
586 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 diff = list(obj.difference(req))
589 if diff:
590 abort(403, _4034_msg % (
591 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500592
Brad Bishop87b63c12016-03-18 14:47:51 -0400593 diff = list(req.difference(obj))
594 if diff:
595 abort(403, _4034_msg % (
596 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500597
CamVan Nguyen249d1322018-03-05 10:08:33 -0600598 for p, v in request.parameter_list.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400599 self.app.property_handler.do_put(
600 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500601
Brad Bishop87b63c12016-03-18 14:47:51 -0400602 def do_delete(self, path):
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500603 deleted = False
604 for bus, interfaces in request.route_data['map'][path].items():
605 if self.bus_has_delete(interfaces):
606 self.delete_on_bus(path, bus)
607 deleted = True
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500608
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500609 #It's OK if some objects didn't have a Delete, but not all
610 if not deleted:
611 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500612
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500613 def bus_has_delete(self, interfaces):
614 return DELETE_IFACE in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500615
Brad Bishop87b63c12016-03-18 14:47:51 -0400616 def delete_on_bus(self, path, bus):
617 obj = self.bus.get_object(bus, path, introspect=False)
618 delete_iface = dbus.Interface(
619 obj, dbus_interface=DELETE_IFACE)
620 delete_iface.Delete()
621
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500622
Brad Bishop2f428582015-12-02 10:56:11 -0500623class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400624 ''' Handles the /login and /logout routes, manages
625 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 rules = ['/login', '/logout']
628 login_str = "User '%s' logged %s"
629 bad_passwd_str = "Invalid username or password"
630 no_user_str = "No user logged in"
631 bad_json_str = "Expecting request format { 'data': " \
632 "[<username>, <password>] }, got '%s'"
633 _require_auth = None
634 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500635
Brad Bishop87b63c12016-03-18 14:47:51 -0400636 def __init__(self, app, bus):
637 super(SessionHandler, self).__init__(
638 app, bus)
639 self.hmac_key = os.urandom(128)
640 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500641
Brad Bishop87b63c12016-03-18 14:47:51 -0400642 @staticmethod
643 def authenticate(username, clear):
644 try:
645 encoded = spwd.getspnam(username)[1]
646 return encoded == crypt.crypt(clear, encoded)
647 except KeyError:
648 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500649
Brad Bishop87b63c12016-03-18 14:47:51 -0400650 def invalidate_session(self, session):
651 try:
652 self.session_store.remove(session)
653 except ValueError:
654 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500655
Brad Bishop87b63c12016-03-18 14:47:51 -0400656 def new_session(self):
657 sid = os.urandom(32)
658 if self.MAX_SESSIONS <= len(self.session_store):
659 self.session_store.pop()
660 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500661
Brad Bishop87b63c12016-03-18 14:47:51 -0400662 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500663
Brad Bishop87b63c12016-03-18 14:47:51 -0400664 def get_session(self, sid):
665 sids = [x['sid'] for x in self.session_store]
666 try:
667 return self.session_store[sids.index(sid)]
668 except ValueError:
669 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500670
Brad Bishop87b63c12016-03-18 14:47:51 -0400671 def get_session_from_cookie(self):
672 return self.get_session(
673 request.get_cookie(
674 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500675
Brad Bishop87b63c12016-03-18 14:47:51 -0400676 def do_post(self, **kw):
677 if request.path == '/login':
678 return self.do_login(**kw)
679 else:
680 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500681
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 def do_logout(self, **kw):
683 session = self.get_session_from_cookie()
684 if session is not None:
685 user = session['user']
686 self.invalidate_session(session)
687 response.delete_cookie('sid')
688 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500689
Brad Bishop87b63c12016-03-18 14:47:51 -0400690 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500691
Brad Bishop87b63c12016-03-18 14:47:51 -0400692 def do_login(self, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400693 if len(request.parameter_list) != 2:
694 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500695
Brad Bishop87b63c12016-03-18 14:47:51 -0400696 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400697 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500698
Brad Bishop87b63c12016-03-18 14:47:51 -0400699 user = request.parameter_list[0]
700 session = self.new_session()
701 session['user'] = user
702 response.set_cookie(
703 'sid', session['sid'], secret=self.hmac_key,
704 secure=True,
705 httponly=True)
706 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500707
Brad Bishop87b63c12016-03-18 14:47:51 -0400708 def find(self, **kw):
709 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500710
Brad Bishop87b63c12016-03-18 14:47:51 -0400711 def setup(self, **kw):
712 pass
713
Brad Bishop2f428582015-12-02 10:56:11 -0500714
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500715class ImageUploadUtils:
716 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500717
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500718 file_loc = '/tmp/images'
719 file_prefix = 'img'
720 file_suffix = ''
Adriana Kobylak53693892018-03-12 13:05:50 -0500721 signal = None
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500722
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500723 @classmethod
724 def do_upload(cls, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500725 def cleanup():
726 os.close(handle)
727 if cls.signal:
728 cls.signal.remove()
729 cls.signal = None
730
731 def signal_callback(path, a, **kw):
732 # Just interested on the first Version interface created which is
733 # triggered when the file is uploaded. This helps avoid getting the
734 # wrong information for multiple upload requests in a row.
735 if "xyz.openbmc_project.Software.Version" in a and \
736 "xyz.openbmc_project.Software.Activation" not in a:
737 paths.append(path)
738
739 while cls.signal:
740 # Serialize uploads by waiting for the signal to be cleared.
741 # This makes it easier to ensure that the version information
742 # is the right one instead of the data from another upload request.
743 gevent.sleep(1)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500744 if not os.path.exists(cls.file_loc):
Gunnar Millsfb515792017-11-09 15:52:17 -0600745 abort(500, "Error Directory not found")
Adriana Kobylak53693892018-03-12 13:05:50 -0500746 paths = []
747 bus = dbus.SystemBus()
748 cls.signal = bus.add_signal_receiver(
749 signal_callback,
750 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
751 signal_name='InterfacesAdded',
752 path=SOFTWARE_PATH)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500753 if not filename:
754 handle, filename = tempfile.mkstemp(cls.file_suffix,
755 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500756 else:
757 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500758 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500759 try:
760 file_contents = request.body.read()
761 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500762 os.write(handle, file_contents)
Adriana Kobylak53693892018-03-12 13:05:50 -0500763 # Close file after writing, the image manager process watches for
764 # the close event to know the upload is complete.
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500765 os.close(handle)
Adriana Kobylak53693892018-03-12 13:05:50 -0500766 except (IOError, ValueError) as e:
767 cleanup()
768 abort(400, str(e))
769 except Exception:
770 cleanup()
771 abort(400, "Unexpected Error")
772 loop = gobject.MainLoop()
773 gcontext = loop.get_context()
774 count = 0
775 version_id = ''
776 while loop is not None:
777 try:
778 if gcontext.pending():
779 gcontext.iteration()
780 if not paths:
781 gevent.sleep(1)
782 else:
783 version_id = os.path.basename(paths.pop())
784 break
785 count += 1
786 if count == 10:
787 break
788 except Exception:
789 break
790 cls.signal.remove()
791 cls.signal = None
Adriana Kobylak97fe4352018-04-10 10:44:11 -0500792 if version_id:
793 return version_id
794 else:
795 abort(400, "Version already exists or failed to be extracted")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500796
797
798class ImagePostHandler(RouteHandler):
799 ''' Handles the /upload/image route. '''
800
801 verbs = ['POST']
802 rules = ['/upload/image']
803 content_type = 'application/octet-stream'
804
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500805 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500806 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500807 app, bus, self.verbs, self.rules, self.content_type)
808
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500809 def do_post(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500810 return ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500811
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500812 def find(self, **kw):
813 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500814
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500815 def setup(self, **kw):
816 pass
817
818
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500819class EventNotifier:
820 keyNames = {}
821 keyNames['event'] = 'event'
822 keyNames['path'] = 'path'
823 keyNames['intfMap'] = 'interfaces'
824 keyNames['propMap'] = 'properties'
825 keyNames['intf'] = 'interface'
826
827 def __init__(self, wsock, filters):
828 self.wsock = wsock
829 self.paths = filters.get("paths", [])
830 self.interfaces = filters.get("interfaces", [])
831 if not self.paths:
832 self.paths.append(None)
833 bus = dbus.SystemBus()
834 # Add a signal receiver for every path the client is interested in
835 for path in self.paths:
836 bus.add_signal_receiver(
837 self.interfaces_added_handler,
838 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
839 signal_name='InterfacesAdded',
840 path=path)
841 bus.add_signal_receiver(
842 self.properties_changed_handler,
843 dbus_interface=dbus.PROPERTIES_IFACE,
844 signal_name='PropertiesChanged',
845 path=path,
846 path_keyword='path')
847 loop = gobject.MainLoop()
848 # gobject's mainloop.run() will block the entire process, so the gevent
849 # scheduler and hence greenlets won't execute. The while-loop below
850 # works around this limitation by using gevent's sleep, instead of
851 # calling loop.run()
852 gcontext = loop.get_context()
853 while loop is not None:
854 try:
855 if gcontext.pending():
856 gcontext.iteration()
857 else:
858 # gevent.sleep puts only the current greenlet to sleep,
859 # not the entire process.
860 gevent.sleep(5)
861 except WebSocketError:
862 break
863
864 def interfaces_added_handler(self, path, iprops, **kw):
865 ''' If the client is interested in these changes, respond to the
866 client. This handles d-bus interface additions.'''
867 if (not self.interfaces) or \
868 (not set(iprops).isdisjoint(self.interfaces)):
869 response = {}
870 response[self.keyNames['event']] = "InterfacesAdded"
871 response[self.keyNames['path']] = path
872 response[self.keyNames['intfMap']] = iprops
873 try:
874 self.wsock.send(json.dumps(response))
875 except WebSocketError:
876 return
877
878 def properties_changed_handler(self, interface, new, old, **kw):
879 ''' If the client is interested in these changes, respond to the
880 client. This handles d-bus property changes. '''
881 if (not self.interfaces) or (interface in self.interfaces):
882 path = str(kw['path'])
883 response = {}
884 response[self.keyNames['event']] = "PropertiesChanged"
885 response[self.keyNames['path']] = path
886 response[self.keyNames['intf']] = interface
887 response[self.keyNames['propMap']] = new
888 try:
889 self.wsock.send(json.dumps(response))
890 except WebSocketError:
891 return
892
893
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500894class EventHandler(RouteHandler):
895 ''' Handles the /subscribe route, for clients to be able
896 to subscribe to BMC events. '''
897
898 verbs = ['GET']
899 rules = ['/subscribe']
900
901 def __init__(self, app, bus):
902 super(EventHandler, self).__init__(
903 app, bus, self.verbs, self.rules)
904
905 def find(self, **kw):
906 pass
907
908 def setup(self, **kw):
909 pass
910
911 def do_get(self):
912 wsock = request.environ.get('wsgi.websocket')
913 if not wsock:
914 abort(400, 'Expected WebSocket request.')
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500915 filters = wsock.receive()
916 filters = json.loads(filters)
917 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500918
919
Deepak Kodihalli5c518f62018-04-23 03:26:38 -0500920class HostConsoleHandler(RouteHandler):
921 ''' Handles the /console route, for clients to be able
922 read/write the host serial console. The way this is
923 done is by exposing a websocket that's mirrored to an
924 abstract UNIX domain socket, which is the source for
925 the console data. '''
926
927 verbs = ['GET']
928 # Naming the route console0, because the numbering will help
929 # on multi-bmc/multi-host systems.
930 rules = ['/console0']
931
932 def __init__(self, app, bus):
933 super(HostConsoleHandler, self).__init__(
934 app, bus, self.verbs, self.rules)
935
936 def find(self, **kw):
937 pass
938
939 def setup(self, **kw):
940 pass
941
942 def read_wsock(self, wsock, sock):
943 while True:
944 try:
945 incoming = wsock.receive()
946 if incoming:
947 # Read websocket, write to UNIX socket
948 sock.send(incoming)
949 except Exception as e:
950 sock.close()
951 return
952
953 def read_sock(self, sock, wsock):
954 max_sock_read_len = 4096
955 while True:
956 try:
957 outgoing = sock.recv(max_sock_read_len)
958 if outgoing:
959 # Read UNIX socket, write to websocket
960 wsock.send(outgoing)
961 except Exception as e:
962 wsock.close()
963 return
964
965 def send_ping(self, wsock) :
966 # Most webservers close websockets after 60 seconds of
967 # inactivity. Make sure to send a ping before that.
968 timeout = 45
969 payload = "ping"
970 # the ping payload can be anything, the receiver has to just
971 # return the same back.
972 while True:
973 gevent.sleep(timeout)
974 wsock.send_frame(payload, wsock.OPCODE_PING)
975
976 def do_get(self):
977 wsock = request.environ.get('wsgi.websocket')
978 if not wsock:
979 abort(400, 'Expected WebSocket based request.')
980
981 # A UNIX domain socket structure defines a 108-byte pathname. The
982 # server in this case, obmc-console-server, expects a 108-byte path.
983 socket_name = "\0obmc-console"
984 trailing_bytes = "\0" * (108 - len(socket_name))
985 socket_path = socket_name + trailing_bytes
986 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
987
988 try:
989 sock.connect(socket_path)
990 except Exception as e:
991 abort(500, str(e))
992
993 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
994 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
995 ping_sender = Greenlet.spawn(self.send_ping, wsock)
996 gevent.joinall([wsock_reader, sock_reader, ping_sender])
997
998
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500999class ImagePutHandler(RouteHandler):
1000 ''' Handles the /upload/image/<filename> route. '''
1001
1002 verbs = ['PUT']
1003 rules = ['/upload/image/<filename>']
1004 content_type = 'application/octet-stream'
1005
1006 def __init__(self, app, bus):
1007 super(ImagePutHandler, self).__init__(
1008 app, bus, self.verbs, self.rules, self.content_type)
1009
1010 def do_put(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -05001011 return ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -05001012
1013 def find(self, **kw):
1014 pass
1015
1016 def setup(self, **kw):
1017 pass
1018
1019
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001020class DownloadDumpHandler(RouteHandler):
1021 ''' Handles the /download/dump route. '''
1022
1023 verbs = 'GET'
1024 rules = ['/download/dump/<dumpid>']
1025 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -05001026 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -04001027 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001028
1029 def __init__(self, app, bus):
1030 super(DownloadDumpHandler, self).__init__(
1031 app, bus, self.verbs, self.rules, self.content_type)
1032
1033 def do_get(self, dumpid):
1034 return self.do_download(dumpid)
1035
1036 def find(self, **kw):
1037 pass
1038
1039 def setup(self, **kw):
1040 pass
1041
1042 def do_download(self, dumpid):
1043 dump_loc = os.path.join(self.dump_loc, dumpid)
1044 if not os.path.exists(dump_loc):
1045 abort(404, "Path not found")
1046
1047 files = os.listdir(dump_loc)
1048 num_files = len(files)
1049 if num_files == 0:
1050 abort(404, "Dump not found")
1051
1052 return static_file(os.path.basename(files[0]), root=dump_loc,
1053 download=True, mimetype=self.content_type)
1054
1055
Matt Spinlerd41643e2018-02-02 13:51:38 -06001056class WebHandler(RouteHandler):
1057 ''' Handles the routes for the web UI files. '''
1058
1059 verbs = 'GET'
1060
1061 # Match only what we know are web files, so everything else
1062 # can get routed to the REST handlers.
1063 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1064 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1065 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1066 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1067 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1068 '/<filename:re:.+\.ico>']
1069
1070 # The mimetypes module knows about most types, but not these
1071 content_types = {
1072 '.eot': 'application/vnd.ms-fontobject',
1073 '.woff': 'application/x-font-woff',
1074 '.woff2': 'application/x-font-woff2',
1075 '.ttf': 'application/x-font-ttf',
1076 '.map': 'application/json'
1077 }
1078
1079 _require_auth = None
1080 suppress_json_resp = True
1081
1082 def __init__(self, app, bus):
1083 super(WebHandler, self).__init__(
1084 app, bus, self.verbs, self.rules)
1085
1086 def get_type(self, filename):
1087 ''' Returns the content type and encoding for a file '''
1088
1089 content_type, encoding = mimetypes.guess_type(filename)
1090
1091 # Try our own list if mimetypes didn't recognize it
1092 if content_type is None:
1093 if filename[-3:] == '.gz':
1094 filename = filename[:-3]
1095 extension = filename[filename.rfind('.'):]
1096 content_type = self.content_types.get(extension, None)
1097
1098 return content_type, encoding
1099
1100 def do_get(self, filename='index.html'):
1101
1102 # If a gzipped version exists, use that instead.
1103 # Possible future enhancement: if the client doesn't
1104 # accept compressed files, unzip it ourselves before sending.
1105 if not os.path.exists(os.path.join(www_base_path, filename)):
1106 filename = filename + '.gz'
1107
1108 # Though bottle should protect us, ensure path is valid
1109 realpath = os.path.realpath(filename)
1110 if realpath[0] == '/':
1111 realpath = realpath[1:]
1112 if not os.path.exists(os.path.join(www_base_path, realpath)):
1113 abort(404, "Path not found")
1114
1115 mimetype, encoding = self.get_type(filename)
1116
1117 # Couldn't find the type - let static_file() deal with it,
1118 # though this should never happen.
1119 if mimetype is None:
1120 print("Can't figure out content-type for %s" % filename)
1121 mimetype = 'auto'
1122
1123 # This call will set several header fields for us,
1124 # including the charset if the type is text.
1125 response = static_file(filename, www_base_path, mimetype)
1126
1127 # static_file() will only set the encoding if the
1128 # mimetype was auto, so set it here.
1129 if encoding is not None:
1130 response.set_header('Content-Encoding', encoding)
1131
1132 return response
1133
1134 def find(self, **kw):
1135 pass
1136
1137 def setup(self, **kw):
1138 pass
1139
1140
Brad Bishop2f428582015-12-02 10:56:11 -05001141class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001142 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -05001143
Brad Bishop87b63c12016-03-18 14:47:51 -04001144 name = 'authorization'
1145 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -05001146
Brad Bishop87b63c12016-03-18 14:47:51 -04001147 class Compose:
1148 def __init__(self, validators, callback, session_mgr):
1149 self.validators = validators
1150 self.callback = callback
1151 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -05001152
Brad Bishop87b63c12016-03-18 14:47:51 -04001153 def __call__(self, *a, **kw):
1154 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1155 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001156 if request.method != 'OPTIONS':
1157 for x in self.validators:
1158 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001159
Brad Bishop87b63c12016-03-18 14:47:51 -04001160 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001161
Brad Bishop87b63c12016-03-18 14:47:51 -04001162 def apply(self, callback, route):
1163 undecorated = route.get_undecorated_callback()
1164 if not isinstance(undecorated, RouteHandler):
1165 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001166
Brad Bishop87b63c12016-03-18 14:47:51 -04001167 auth_types = getattr(
1168 undecorated, '_require_auth', None)
1169 if not auth_types:
1170 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001171
Brad Bishop87b63c12016-03-18 14:47:51 -04001172 return self.Compose(
1173 auth_types, callback, undecorated.app.session_handler)
1174
Brad Bishop2f428582015-12-02 10:56:11 -05001175
Brad Bishopd0c404a2017-02-21 09:23:25 -05001176class CorsPlugin(object):
1177 ''' Add CORS headers. '''
1178
1179 name = 'cors'
1180 api = 2
1181
1182 @staticmethod
1183 def process_origin():
1184 origin = request.headers.get('Origin')
1185 if origin:
1186 response.add_header('Access-Control-Allow-Origin', origin)
1187 response.add_header(
1188 'Access-Control-Allow-Credentials', 'true')
1189
1190 @staticmethod
1191 def process_method_and_headers(verbs):
1192 method = request.headers.get('Access-Control-Request-Method')
1193 headers = request.headers.get('Access-Control-Request-Headers')
1194 if headers:
1195 headers = [x.lower() for x in headers.split(',')]
1196
1197 if method in verbs \
1198 and headers == ['content-type']:
1199 response.add_header('Access-Control-Allow-Methods', method)
1200 response.add_header(
1201 'Access-Control-Allow-Headers', 'Content-Type')
Ratan Gupta91b46f82018-01-14 12:52:23 +05301202 response.add_header('X-Frame-Options', 'deny')
1203 response.add_header('X-Content-Type-Options', 'nosniff')
1204 response.add_header('X-XSS-Protection', '1; mode=block')
1205 response.add_header(
1206 'Content-Security-Policy', "default-src 'self'")
1207 response.add_header(
1208 'Strict-Transport-Security',
1209 'max-age=31536000; includeSubDomains; preload')
Brad Bishopd0c404a2017-02-21 09:23:25 -05001210
1211 def __init__(self, app):
1212 app.install_error_callback(self.error_callback)
1213
1214 def apply(self, callback, route):
1215 undecorated = route.get_undecorated_callback()
1216 if not isinstance(undecorated, RouteHandler):
1217 return callback
1218
1219 if not getattr(undecorated, '_enable_cors', None):
1220 return callback
1221
1222 def wrap(*a, **kw):
1223 self.process_origin()
1224 self.process_method_and_headers(undecorated._verbs)
1225 return callback(*a, **kw)
1226
1227 return wrap
1228
1229 def error_callback(self, **kw):
1230 self.process_origin()
1231
1232
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001233class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001234 ''' Ensures request content satisfies the OpenBMC json api format. '''
1235 name = 'json_api_request'
1236 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001237
Brad Bishop87b63c12016-03-18 14:47:51 -04001238 error_str = "Expecting request format { 'data': <value> }, got '%s'"
1239 type_error_str = "Unsupported Content-Type: '%s'"
1240 json_type = "application/json"
1241 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001242
Brad Bishop87b63c12016-03-18 14:47:51 -04001243 @staticmethod
1244 def content_expected():
1245 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001246
Brad Bishop87b63c12016-03-18 14:47:51 -04001247 def validate_request(self):
1248 if request.content_length > 0 and \
1249 request.content_type != self.json_type:
1250 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001251
Brad Bishop87b63c12016-03-18 14:47:51 -04001252 try:
1253 request.parameter_list = request.json.get('data')
CamVan Nguyen249d1322018-03-05 10:08:33 -06001254 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -04001255 abort(400, str(e))
1256 except (AttributeError, KeyError, TypeError):
1257 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001258
Brad Bishop87b63c12016-03-18 14:47:51 -04001259 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001260 content_type = getattr(
1261 route.get_undecorated_callback(), '_content_type', None)
1262 if self.json_type != content_type:
1263 return callback
1264
Brad Bishop87b63c12016-03-18 14:47:51 -04001265 verbs = getattr(
1266 route.get_undecorated_callback(), '_verbs', None)
1267 if verbs is None:
1268 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001269
Brad Bishop87b63c12016-03-18 14:47:51 -04001270 if not set(self.request_methods).intersection(verbs):
1271 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001272
Brad Bishop87b63c12016-03-18 14:47:51 -04001273 def wrap(*a, **kw):
1274 if self.content_expected():
1275 self.validate_request()
1276 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001277
Brad Bishop87b63c12016-03-18 14:47:51 -04001278 return wrap
1279
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001280
1281class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001282 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1283 name = 'json_api_method_request'
1284 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001285
Brad Bishop87b63c12016-03-18 14:47:51 -04001286 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001287 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001288
Brad Bishop87b63c12016-03-18 14:47:51 -04001289 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001290 content_type = getattr(
1291 route.get_undecorated_callback(), '_content_type', None)
1292 if self.json_type != content_type:
1293 return callback
1294
Brad Bishop87b63c12016-03-18 14:47:51 -04001295 request_type = getattr(
1296 route.get_undecorated_callback(), 'request_type', None)
1297 if request_type is None:
1298 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001299
Brad Bishop87b63c12016-03-18 14:47:51 -04001300 def validate_request():
1301 if not isinstance(request.parameter_list, request_type):
1302 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001303
Brad Bishop87b63c12016-03-18 14:47:51 -04001304 def wrap(*a, **kw):
1305 if JsonApiRequestPlugin.content_expected():
1306 validate_request()
1307 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001308
Brad Bishop87b63c12016-03-18 14:47:51 -04001309 return wrap
1310
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001311
Brad Bishop080a48e2017-02-21 22:34:43 -05001312class JsonErrorsPlugin(JSONPlugin):
1313 ''' Extend the Bottle JSONPlugin such that it also encodes error
1314 responses. '''
1315
1316 def __init__(self, app, **kw):
1317 super(JsonErrorsPlugin, self).__init__(**kw)
1318 self.json_opts = {
CamVan Nguyen249d1322018-03-05 10:08:33 -06001319 x: y for x, y in kw.items()
Brad Bishop080a48e2017-02-21 22:34:43 -05001320 if x in ['indent', 'sort_keys']}
1321 app.install_error_callback(self.error_callback)
1322
1323 def error_callback(self, response_object, response_body, **kw):
1324 response_body['body'] = json.dumps(response_object, **self.json_opts)
1325 response.content_type = 'application/json'
1326
1327
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001328class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001329 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001330 name = 'json_api_response'
1331 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001332
Brad Bishopd4c1c552017-02-21 00:07:28 -05001333 @staticmethod
1334 def has_body():
1335 return request.method not in ['OPTIONS']
1336
Brad Bishop080a48e2017-02-21 22:34:43 -05001337 def __init__(self, app):
1338 app.install_error_callback(self.error_callback)
1339
Brad Bishop87b63c12016-03-18 14:47:51 -04001340 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001341 skip = getattr(
1342 route.get_undecorated_callback(), 'suppress_json_resp', None)
1343 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001344 return callback
1345
Brad Bishop87b63c12016-03-18 14:47:51 -04001346 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001347 data = callback(*a, **kw)
1348 if self.has_body():
1349 resp = {'data': data}
1350 resp['status'] = 'ok'
1351 resp['message'] = response.status_line
1352 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001353 return wrap
1354
Brad Bishop080a48e2017-02-21 22:34:43 -05001355 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001356 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001357 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001358 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001359 if error.status_code == 500:
1360 response_object['data']['exception'] = repr(error.exception)
1361 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001362
Brad Bishop87b63c12016-03-18 14:47:51 -04001363
Brad Bishop080a48e2017-02-21 22:34:43 -05001364class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001365 ''' Json javascript wrapper. '''
1366 name = 'jsonp'
1367 api = 2
1368
Brad Bishop080a48e2017-02-21 22:34:43 -05001369 def __init__(self, app, **kw):
1370 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001371
1372 @staticmethod
1373 def to_jsonp(json):
1374 jwrapper = request.query.callback or None
1375 if(jwrapper):
1376 response.set_header('Content-Type', 'application/javascript')
1377 json = jwrapper + '(' + json + ');'
1378 return json
1379
1380 def apply(self, callback, route):
1381 def wrap(*a, **kw):
1382 return self.to_jsonp(callback(*a, **kw))
1383 return wrap
1384
Brad Bishop080a48e2017-02-21 22:34:43 -05001385 def error_callback(self, response_body, **kw):
1386 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001387
1388
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001389class ContentCheckerPlugin(object):
1390 ''' Ensures that a route is associated with the expected content-type
1391 header. '''
1392 name = 'content_checker'
1393 api = 2
1394
1395 class Checker:
1396 def __init__(self, type, callback):
1397 self.expected_type = type
1398 self.callback = callback
1399 self.error_str = "Expecting content type '%s', got '%s'"
1400
1401 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001402 if request.method in ['PUT', 'POST', 'PATCH'] and \
1403 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001404 self.expected_type != request.content_type:
1405 abort(415, self.error_str % (self.expected_type,
1406 request.content_type))
1407
1408 return self.callback(*a, **kw)
1409
1410 def apply(self, callback, route):
1411 content_type = getattr(
1412 route.get_undecorated_callback(), '_content_type', None)
1413
1414 return self.Checker(content_type, callback)
1415
1416
Brad Bishop2c6fc762016-08-29 15:53:25 -04001417class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001418 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001419 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001420
1421 self.have_wsock = kw.get('have_wsock', False)
1422
Brad Bishop2ddfa002016-08-29 15:11:55 -04001423 self.bus = dbus.SystemBus()
1424 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001425 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001426
Brad Bishop87b63c12016-03-18 14:47:51 -04001427 self.install_hooks()
1428 self.install_plugins()
1429 self.create_handlers()
1430 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001431
Brad Bishop87b63c12016-03-18 14:47:51 -04001432 def install_plugins(self):
1433 # install json api plugins
1434 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001435 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001436 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001437 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001438 self.install(JsonpPlugin(self, **json_kw))
1439 self.install(JsonErrorsPlugin(self, **json_kw))
1440 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001441 self.install(JsonApiRequestPlugin())
1442 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001443
Brad Bishop87b63c12016-03-18 14:47:51 -04001444 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001445 self.error_handler_type = type(self.default_error_handler)
1446 self.original_error_handler = self.default_error_handler
1447 self.default_error_handler = self.error_handler_type(
1448 self.custom_error_handler, self, Bottle)
1449
Brad Bishop87b63c12016-03-18 14:47:51 -04001450 self.real_router_match = self.router.match
1451 self.router.match = self.custom_router_match
1452 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001453
Brad Bishop87b63c12016-03-18 14:47:51 -04001454 def create_handlers(self):
1455 # create route handlers
1456 self.session_handler = SessionHandler(self, self.bus)
Matt Spinlerd41643e2018-02-02 13:51:38 -06001457 self.web_handler = WebHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001458 self.directory_handler = DirectoryHandler(self, self.bus)
1459 self.list_names_handler = ListNamesHandler(self, self.bus)
1460 self.list_handler = ListHandler(self, self.bus)
1461 self.method_handler = MethodHandler(self, self.bus)
1462 self.property_handler = PropertyHandler(self, self.bus)
1463 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001464 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1465 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001466 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001467 if self.have_wsock:
1468 self.event_handler = EventHandler(self, self.bus)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001469 self.host_console_handler = HostConsoleHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001470 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001471
Brad Bishop87b63c12016-03-18 14:47:51 -04001472 def install_handlers(self):
1473 self.session_handler.install()
Matt Spinlerd41643e2018-02-02 13:51:38 -06001474 self.web_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001475 self.directory_handler.install()
1476 self.list_names_handler.install()
1477 self.list_handler.install()
1478 self.method_handler.install()
1479 self.property_handler.install()
1480 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001481 self.image_upload_post_handler.install()
1482 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001483 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001484 if self.have_wsock:
1485 self.event_handler.install()
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001486 self.host_console_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001487 # this has to come last, since it matches everything
1488 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001489
Brad Bishop080a48e2017-02-21 22:34:43 -05001490 def install_error_callback(self, callback):
1491 self.error_callbacks.insert(0, callback)
1492
Brad Bishop87b63c12016-03-18 14:47:51 -04001493 def custom_router_match(self, environ):
1494 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1495 needed doesn't work for us since the instance rules match
1496 everything. This monkey-patch lets the route handler figure
1497 out which response is needed. This could be accomplished
1498 with a hook but that would require calling the router match
1499 function twice.
1500 '''
1501 route, args = self.real_router_match(environ)
1502 if isinstance(route.callback, RouteHandler):
1503 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001504
Brad Bishop87b63c12016-03-18 14:47:51 -04001505 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001506
Brad Bishop080a48e2017-02-21 22:34:43 -05001507 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001508 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001509 error handler. '''
1510
1511 response_object = {}
1512 response_body = {}
1513 for x in self.error_callbacks:
1514 x(error=error,
1515 response_object=response_object,
1516 response_body=response_body)
1517
1518 return response_body.get('body', "")
1519
Brad Bishop87b63c12016-03-18 14:47:51 -04001520 @staticmethod
1521 def strip_extra_slashes():
1522 path = request.environ['PATH_INFO']
1523 trailing = ("", "/")[path[-1] == '/']
CamVan Nguyen249d1322018-03-05 10:08:33 -06001524 parts = list(filter(bool, path.split('/')))
Brad Bishop87b63c12016-03-18 14:47:51 -04001525 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing