blob: f761df9217b09f03f833f6c9144b9da2822a783c [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
Nagaraju Goruganti0cf702c2018-04-17 22:27:08 -050034import fnmatch
Deepak Kodihalli639b5022017-10-13 06:40:26 -050035have_wsock = True
36try:
37 from geventwebsocket import WebSocketError
38except ImportError:
39 have_wsock = False
40if have_wsock:
41 from dbus.mainloop.glib import DBusGMainLoop
42 DBusGMainLoop(set_as_default=True)
CamVan Nguyen249d1322018-03-05 10:08:33 -060043 # TODO: openbmc/openbmc#2994 remove python 2 support
44 try: # python 2
45 import gobject
46 except ImportError: # python 3
47 from gi.repository import GObject as gobject
Deepak Kodihalli639b5022017-10-13 06:40:26 -050048 import gevent
Deepak Kodihalli5c518f62018-04-23 03:26:38 -050049 from gevent import socket
50 from gevent import Greenlet
Brad Bishopaa65f6e2015-10-27 16:28:51 -040051
Adriana Kobylakf92cf4d2017-12-13 11:46:50 -060052DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050053DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
54DBUS_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>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400293
Brad Bishop87b63c12016-03-18 14:47:51 -0400294 def __init__(self, app, bus):
295 super(DirectoryHandler, 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='/'):
299 return self.try_mapper_call(
300 self.mapper.get_subtree_paths, path=path, depth=1)
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 ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 verbs = 'GET'
311 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400312
Brad Bishop87b63c12016-03-18 14:47:51 -0400313 def __init__(self, app, bus):
314 super(ListNamesHandler, 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='/'):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600318 return list(self.try_mapper_call(
319 self.mapper.get_subtree, path=path).keys())
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='/'):
325 return request.route_data['map']
326
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400327
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500328class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400329 verbs = 'GET'
330 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400331
Brad Bishop87b63c12016-03-18 14:47:51 -0400332 def __init__(self, app, bus):
333 super(ListHandler, self).__init__(
Brad Bishopc431e1a2017-07-10 16:44:51 -0400334 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400335
Brad Bishop87b63c12016-03-18 14:47:51 -0400336 def find(self, path='/'):
337 return self.try_mapper_call(
338 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400339
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 def setup(self, path='/'):
341 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400344 return {x: y for x, y in self.mapper.enumerate_subtree(
345 path,
346 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400347
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400348
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500349class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400350 verbs = 'POST'
351 rules = '<path:path>/action/<method>'
352 request_type = list
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500353 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400354
Brad Bishop87b63c12016-03-18 14:47:51 -0400355 def __init__(self, app, bus):
356 super(MethodHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500357 app, bus, self.verbs, self.rules, self.content_type)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530358 self.service = ''
359 self.interface = ''
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400360
Brad Bishop87b63c12016-03-18 14:47:51 -0400361 def find(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500362 method_list = []
Gunnar Mills313aadb2018-04-08 14:50:09 -0500363 buses = self.try_mapper_call(
Brad Bishop87b63c12016-03-18 14:47:51 -0400364 self.mapper.get_object, path=path)
Gunnar Mills313aadb2018-04-08 14:50:09 -0500365 for items in buses.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 m = self.find_method_on_bus(path, method, *items)
367 if m:
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500368 method_list.append(m)
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600369 if method_list:
370 return method_list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400371
Brad Bishop87b63c12016-03-18 14:47:51 -0400372 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400373
Brad Bishop87b63c12016-03-18 14:47:51 -0400374 def setup(self, path, method):
Saqib Khan3a00b1f2017-11-04 15:56:21 -0500375 request.route_data['map'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400376
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600377 def do_post(self, path, method, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 try:
Nagaraju Goruganti765c2c82017-11-13 06:17:13 -0600379 args = []
380 if request.parameter_list:
381 args = request.parameter_list
382 # To see if the return type is capable of being merged
383 if len(request.route_data['map']) > 1:
384 results = None
385 for item in request.route_data['map']:
386 tmp = item(*args)
387 if not results:
388 if tmp is not None:
389 results = type(tmp)()
390 if isinstance(results, dict):
391 results = results.update(tmp)
392 elif isinstance(results, list):
393 results = results + tmp
394 elif isinstance(results, type(None)):
395 results = None
396 else:
397 abort(501, 'Don\'t know how to merge method call '
398 'results of {}'.format(type(tmp)))
399 return results
400 # There is only one method
401 return request.route_data['map'][0](*args)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400402
CamVan Nguyen249d1322018-03-05 10:08:33 -0600403 except dbus.exceptions.DBusException as e:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530404 paramlist = []
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500405 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530406
407 signature_list = get_method_signature(self.bus, self.service,
408 path, self.interface,
409 method)
410 if not signature_list:
411 abort(400, "Failed to get method signature: %s" % str(e))
412 if len(signature_list) != len(request.parameter_list):
413 abort(400, "Invalid number of args")
414 converted_value = None
415 try:
416 for index, expected_type in enumerate(signature_list):
417 value = request.parameter_list[index]
418 converted_value = convert_type(expected_type, value)
419 paramlist.append(converted_value)
420 request.parameter_list = paramlist
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600421 self.do_post(path, method, False)
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530422 return
423 except Exception as ex:
Nagaraju Gorugantiab404fa2017-12-14 10:24:40 -0600424 abort(400, "Bad Request/Invalid Args given")
Brad Bishop87b63c12016-03-18 14:47:51 -0400425 abort(400, str(e))
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530426
Brad Bishop87b63c12016-03-18 14:47:51 -0400427 if e.get_dbus_name() == DBUS_TYPE_ERROR:
428 abort(400, str(e))
429 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400430
Brad Bishop87b63c12016-03-18 14:47:51 -0400431 @staticmethod
432 def find_method_in_interface(method, obj, interface, methods):
433 if methods is None:
434 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400435
CamVan Nguyen249d1322018-03-05 10:08:33 -0600436 method = obmc.utils.misc.find_case_insensitive(method, list(methods.keys()))
Brad Bishop87b63c12016-03-18 14:47:51 -0400437 if method is not None:
438 iface = dbus.Interface(obj, interface)
439 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400440
Brad Bishop87b63c12016-03-18 14:47:51 -0400441 def find_method_on_bus(self, path, method, bus, interfaces):
442 obj = self.bus.get_object(bus, path, introspect=False)
443 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
444 data = iface.Introspect()
445 parser = IntrospectionNodeParser(
446 ElementTree.fromstring(data),
Brad Bishopaeb995d2018-04-04 22:28:42 -0400447 intf_match=lambda x: x in interfaces)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600448 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400449 m = self.find_method_in_interface(
450 method, obj, x, y.get('method'))
451 if m:
Ratan Guptaa6a8a4c2017-08-07 08:18:44 +0530452 self.service = bus
453 self.interface = x
Brad Bishop87b63c12016-03-18 14:47:51 -0400454 return m
455
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400456
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500457class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400458 verbs = ['PUT', 'GET']
459 rules = '<path:path>/attr/<prop>'
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500460 content_type = 'application/json'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400461
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 def __init__(self, app, bus):
463 super(PropertyHandler, self).__init__(
Deepak Kodihalli83afbaf2017-04-10 06:37:19 -0500464 app, bus, self.verbs, self.rules, self.content_type)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400465
Brad Bishop87b63c12016-03-18 14:47:51 -0400466 def find(self, path, prop):
467 self.app.instance_handler.setup(path)
468 obj = self.app.instance_handler.do_get(path)
Brad Bishop56ad87f2017-02-21 23:33:29 -0500469 real_name = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600470 prop, list(obj.keys()))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400471
Brad Bishop56ad87f2017-02-21 23:33:29 -0500472 if not real_name:
473 if request.method == 'PUT':
474 abort(403, _4034_msg % ('property', 'created', prop))
475 else:
476 abort(404, _4034_msg % ('property', 'found', prop))
477 return real_name, {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500478
Brad Bishop87b63c12016-03-18 14:47:51 -0400479 def setup(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500480 name, obj = self.find(path, prop)
481 request.route_data['obj'] = obj
482 request.route_data['name'] = name
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 def do_get(self, path, prop):
Brad Bishop56ad87f2017-02-21 23:33:29 -0500485 name = request.route_data['name']
486 return request.route_data['obj'][path][name]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500487
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600488 def do_put(self, path, prop, value=None, retry=True):
Brad Bishop87b63c12016-03-18 14:47:51 -0400489 if value is None:
490 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500491
Brad Bishop87b63c12016-03-18 14:47:51 -0400492 prop, iface, properties_iface = self.get_host_interface(
493 path, prop, request.route_data['map'][path])
494 try:
495 properties_iface.Set(iface, prop, value)
CamVan Nguyen249d1322018-03-05 10:08:33 -0600496 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 abort(400, str(e))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600498 except dbus.exceptions.DBusException as e:
Brad Bishopb7fca9b2018-01-23 12:16:50 -0500499 if e.get_dbus_name() == DBUS_INVALID_ARGS and retry:
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500500 bus_name = properties_iface.bus_name
501 expected_type = get_type_signature_by_introspection(self.bus,
502 bus_name,
503 path,
504 prop)
505 if not expected_type:
506 abort(403, "Failed to get expected type: %s" % str(e))
507 converted_value = None
508 try:
509 converted_value = convert_type(expected_type, value)
Marri Devender Raobc0c6732017-11-20 00:15:47 -0600510 self.do_put(path, prop, converted_value, False)
Leonel Gonzalez0bdef952017-04-18 08:17:49 -0500511 return
512 except Exception as ex:
513 abort(403, "Failed to convert %s to type %s" %
514 (value, expected_type))
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 abort(403, str(e))
516 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500517
Brad Bishop87b63c12016-03-18 14:47:51 -0400518 def get_host_interface(self, path, prop, bus_info):
CamVan Nguyen249d1322018-03-05 10:08:33 -0600519 for bus, interfaces in bus_info.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 obj = self.bus.get_object(bus, path, introspect=True)
521 properties_iface = dbus.Interface(
522 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500523
Brad Bishop87b63c12016-03-18 14:47:51 -0400524 info = self.get_host_interface_on_bus(
525 path, prop, properties_iface, bus, interfaces)
526 if info is not None:
527 prop, iface = info
528 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500529
Brad Bishop87b63c12016-03-18 14:47:51 -0400530 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
531 for i in interfaces:
532 properties = self.try_properties_interface(iface.GetAll, i)
Brad Bishop69cb6d12017-02-21 12:01:52 -0500533 if not properties:
Brad Bishop87b63c12016-03-18 14:47:51 -0400534 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500535 match = obmc.utils.misc.find_case_insensitive(
CamVan Nguyen249d1322018-03-05 10:08:33 -0600536 prop, list(properties.keys()))
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500537 if match is None:
Brad Bishop87b63c12016-03-18 14:47:51 -0400538 continue
Leonel Gonzalez409f6712017-05-24 09:51:55 -0500539 prop = match
Brad Bishop87b63c12016-03-18 14:47:51 -0400540 return prop, i
541
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500542
Brad Bishop2503bd62015-12-16 17:56:12 -0500543class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400544 verbs = ['GET']
545 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500546
Brad Bishop87b63c12016-03-18 14:47:51 -0400547 def __init__(self, app, bus):
548 super(SchemaHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400549 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500550
Brad Bishop87b63c12016-03-18 14:47:51 -0400551 def find(self, path):
552 return self.try_mapper_call(
553 self.mapper.get_object,
554 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500555
Brad Bishop87b63c12016-03-18 14:47:51 -0400556 def setup(self, path):
557 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 def do_get(self, path):
560 schema = {}
CamVan Nguyen249d1322018-03-05 10:08:33 -0600561 for x in request.route_data['map'].keys():
Brad Bishop87b63c12016-03-18 14:47:51 -0400562 obj = self.bus.get_object(x, path, introspect=False)
563 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
564 data = iface.Introspect()
565 parser = IntrospectionNodeParser(
566 ElementTree.fromstring(data))
CamVan Nguyen249d1322018-03-05 10:08:33 -0600567 for x, y in parser.get_interfaces().items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500569
Brad Bishop87b63c12016-03-18 14:47:51 -0400570 return schema
571
Brad Bishop2503bd62015-12-16 17:56:12 -0500572
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500573class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400574 verbs = ['GET', 'PUT', 'DELETE']
575 rules = '<path:path>'
576 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500577
Brad Bishop87b63c12016-03-18 14:47:51 -0400578 def __init__(self, app, bus):
579 super(InstanceHandler, self).__init__(
Brad Bishop529029b2017-07-10 16:46:01 -0400580 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500581
Brad Bishop87b63c12016-03-18 14:47:51 -0400582 def find(self, path, callback=None):
583 return {path: self.try_mapper_call(
584 self.mapper.get_object,
585 callback,
586 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 def setup(self, path):
589 callback = None
590 if request.method == 'PUT':
591 def callback(e, **kw):
592 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 if request.route_data.get('map') is None:
595 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500596
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400598 return self.mapper.enumerate_object(
599 path,
600 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500601
Brad Bishop87b63c12016-03-18 14:47:51 -0400602 def do_put(self, path):
603 # make sure all properties exist in the request
604 obj = set(self.do_get(path).keys())
605 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500606
Brad Bishop87b63c12016-03-18 14:47:51 -0400607 diff = list(obj.difference(req))
608 if diff:
609 abort(403, _4034_msg % (
610 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500611
Brad Bishop87b63c12016-03-18 14:47:51 -0400612 diff = list(req.difference(obj))
613 if diff:
614 abort(403, _4034_msg % (
615 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500616
CamVan Nguyen249d1322018-03-05 10:08:33 -0600617 for p, v in request.parameter_list.items():
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 self.app.property_handler.do_put(
619 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500620
Brad Bishop87b63c12016-03-18 14:47:51 -0400621 def do_delete(self, path):
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500622 deleted = False
623 for bus, interfaces in request.route_data['map'][path].items():
624 if self.bus_has_delete(interfaces):
625 self.delete_on_bus(path, bus)
626 deleted = True
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500627
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500628 #It's OK if some objects didn't have a Delete, but not all
629 if not deleted:
630 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500631
Matt Spinlerb1f6a2c2018-05-14 12:25:21 -0500632 def bus_has_delete(self, interfaces):
633 return DELETE_IFACE in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500634
Brad Bishop87b63c12016-03-18 14:47:51 -0400635 def delete_on_bus(self, path, bus):
636 obj = self.bus.get_object(bus, path, introspect=False)
637 delete_iface = dbus.Interface(
638 obj, dbus_interface=DELETE_IFACE)
639 delete_iface.Delete()
640
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500641
Brad Bishop2f428582015-12-02 10:56:11 -0500642class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400643 ''' Handles the /login and /logout routes, manages
644 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500645
Brad Bishop87b63c12016-03-18 14:47:51 -0400646 rules = ['/login', '/logout']
647 login_str = "User '%s' logged %s"
648 bad_passwd_str = "Invalid username or password"
649 no_user_str = "No user logged in"
650 bad_json_str = "Expecting request format { 'data': " \
651 "[<username>, <password>] }, got '%s'"
Alexander Filippovd08a4562018-03-20 12:02:23 +0300652 bmc_not_ready_str = "BMC is not ready (booting)"
Brad Bishop87b63c12016-03-18 14:47:51 -0400653 _require_auth = None
654 MAX_SESSIONS = 16
Alexander Filippovd08a4562018-03-20 12:02:23 +0300655 BMCSTATE_IFACE = 'xyz.openbmc_project.State.BMC'
656 BMCSTATE_PATH = '/xyz/openbmc_project/state/bmc0'
657 BMCSTATE_PROPERTY = 'CurrentBMCState'
658 BMCSTATE_READY = 'xyz.openbmc_project.State.BMC.BMCState.Ready'
Brad Bishop2f428582015-12-02 10:56:11 -0500659
Brad Bishop87b63c12016-03-18 14:47:51 -0400660 def __init__(self, app, bus):
661 super(SessionHandler, self).__init__(
662 app, bus)
663 self.hmac_key = os.urandom(128)
664 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500665
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 @staticmethod
667 def authenticate(username, clear):
668 try:
669 encoded = spwd.getspnam(username)[1]
670 return encoded == crypt.crypt(clear, encoded)
671 except KeyError:
672 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500673
Brad Bishop87b63c12016-03-18 14:47:51 -0400674 def invalidate_session(self, session):
675 try:
676 self.session_store.remove(session)
677 except ValueError:
678 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500679
Brad Bishop87b63c12016-03-18 14:47:51 -0400680 def new_session(self):
681 sid = os.urandom(32)
682 if self.MAX_SESSIONS <= len(self.session_store):
683 self.session_store.pop()
684 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500685
Brad Bishop87b63c12016-03-18 14:47:51 -0400686 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500687
Brad Bishop87b63c12016-03-18 14:47:51 -0400688 def get_session(self, sid):
689 sids = [x['sid'] for x in self.session_store]
690 try:
691 return self.session_store[sids.index(sid)]
692 except ValueError:
693 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500694
Brad Bishop87b63c12016-03-18 14:47:51 -0400695 def get_session_from_cookie(self):
696 return self.get_session(
697 request.get_cookie(
698 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500699
Brad Bishop87b63c12016-03-18 14:47:51 -0400700 def do_post(self, **kw):
701 if request.path == '/login':
702 return self.do_login(**kw)
703 else:
704 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500705
Brad Bishop87b63c12016-03-18 14:47:51 -0400706 def do_logout(self, **kw):
707 session = self.get_session_from_cookie()
708 if session is not None:
709 user = session['user']
710 self.invalidate_session(session)
711 response.delete_cookie('sid')
712 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500715
Brad Bishop87b63c12016-03-18 14:47:51 -0400716 def do_login(self, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400717 if len(request.parameter_list) != 2:
718 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500719
Brad Bishop87b63c12016-03-18 14:47:51 -0400720 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400721 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500722
Alexander Filippovd08a4562018-03-20 12:02:23 +0300723 force = False
724 try:
725 force = request.json.get('force')
726 except (ValueError, AttributeError, KeyError, TypeError):
727 force = False
728
729 if not force and not self.is_bmc_ready():
730 abort(503, self.bmc_not_ready_str)
731
Brad Bishop87b63c12016-03-18 14:47:51 -0400732 user = request.parameter_list[0]
733 session = self.new_session()
734 session['user'] = user
735 response.set_cookie(
736 'sid', session['sid'], secret=self.hmac_key,
737 secure=True,
738 httponly=True)
739 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500740
Alexander Filippovd08a4562018-03-20 12:02:23 +0300741 def is_bmc_ready(self):
742 if not self.app.with_bmc_check:
743 return True
744
745 try:
746 obj = self.bus.get_object(self.BMCSTATE_IFACE, self.BMCSTATE_PATH)
747 iface = dbus.Interface(obj, dbus.PROPERTIES_IFACE)
748 state = iface.Get(self.BMCSTATE_IFACE, self.BMCSTATE_PROPERTY)
749 if state == self.BMCSTATE_READY:
750 return True
751
752 except dbus.exceptions.DBusException:
753 pass
754
755 return False
756
Brad Bishop87b63c12016-03-18 14:47:51 -0400757 def find(self, **kw):
758 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500759
Brad Bishop87b63c12016-03-18 14:47:51 -0400760 def setup(self, **kw):
761 pass
762
Brad Bishop2f428582015-12-02 10:56:11 -0500763
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500764class ImageUploadUtils:
765 ''' Provides common utils for image upload. '''
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500766
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500767 file_loc = '/tmp/images'
768 file_prefix = 'img'
769 file_suffix = ''
Adriana Kobylak53693892018-03-12 13:05:50 -0500770 signal = None
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500771
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500772 @classmethod
773 def do_upload(cls, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500774 def cleanup():
775 os.close(handle)
776 if cls.signal:
777 cls.signal.remove()
778 cls.signal = None
779
780 def signal_callback(path, a, **kw):
781 # Just interested on the first Version interface created which is
782 # triggered when the file is uploaded. This helps avoid getting the
783 # wrong information for multiple upload requests in a row.
784 if "xyz.openbmc_project.Software.Version" in a and \
785 "xyz.openbmc_project.Software.Activation" not in a:
786 paths.append(path)
787
788 while cls.signal:
789 # Serialize uploads by waiting for the signal to be cleared.
790 # This makes it easier to ensure that the version information
791 # is the right one instead of the data from another upload request.
792 gevent.sleep(1)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500793 if not os.path.exists(cls.file_loc):
Gunnar Millsfb515792017-11-09 15:52:17 -0600794 abort(500, "Error Directory not found")
Adriana Kobylak53693892018-03-12 13:05:50 -0500795 paths = []
796 bus = dbus.SystemBus()
797 cls.signal = bus.add_signal_receiver(
798 signal_callback,
799 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
800 signal_name='InterfacesAdded',
801 path=SOFTWARE_PATH)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500802 if not filename:
803 handle, filename = tempfile.mkstemp(cls.file_suffix,
804 cls.file_prefix, cls.file_loc)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500805 else:
806 filename = os.path.join(cls.file_loc, filename)
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500807 handle = os.open(filename, os.O_WRONLY | os.O_CREAT)
Leonel Gonzalez0b62edf2017-06-08 15:10:03 -0500808 try:
809 file_contents = request.body.read()
810 request.body.close()
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500811 os.write(handle, file_contents)
Adriana Kobylak53693892018-03-12 13:05:50 -0500812 # Close file after writing, the image manager process watches for
813 # the close event to know the upload is complete.
Gunnar Millsb66b18c2017-08-21 16:17:21 -0500814 os.close(handle)
Adriana Kobylak53693892018-03-12 13:05:50 -0500815 except (IOError, ValueError) as e:
816 cleanup()
817 abort(400, str(e))
818 except Exception:
819 cleanup()
820 abort(400, "Unexpected Error")
821 loop = gobject.MainLoop()
822 gcontext = loop.get_context()
823 count = 0
824 version_id = ''
825 while loop is not None:
826 try:
827 if gcontext.pending():
828 gcontext.iteration()
829 if not paths:
830 gevent.sleep(1)
831 else:
832 version_id = os.path.basename(paths.pop())
833 break
834 count += 1
835 if count == 10:
836 break
837 except Exception:
838 break
839 cls.signal.remove()
840 cls.signal = None
Adriana Kobylak97fe4352018-04-10 10:44:11 -0500841 if version_id:
842 return version_id
843 else:
844 abort(400, "Version already exists or failed to be extracted")
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500845
846
847class ImagePostHandler(RouteHandler):
848 ''' Handles the /upload/image route. '''
849
850 verbs = ['POST']
851 rules = ['/upload/image']
852 content_type = 'application/octet-stream'
853
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500854 def __init__(self, app, bus):
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500855 super(ImagePostHandler, self).__init__(
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500856 app, bus, self.verbs, self.rules, self.content_type)
857
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500858 def do_post(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -0500859 return ImageUploadUtils.do_upload()
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500860
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500861 def find(self, **kw):
862 pass
Deepak Kodihalli1af301a2017-04-11 07:29:01 -0500863
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -0500864 def setup(self, **kw):
865 pass
866
867
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500868class EventNotifier:
869 keyNames = {}
870 keyNames['event'] = 'event'
871 keyNames['path'] = 'path'
872 keyNames['intfMap'] = 'interfaces'
873 keyNames['propMap'] = 'properties'
874 keyNames['intf'] = 'interface'
875
876 def __init__(self, wsock, filters):
877 self.wsock = wsock
878 self.paths = filters.get("paths", [])
879 self.interfaces = filters.get("interfaces", [])
880 if not self.paths:
881 self.paths.append(None)
882 bus = dbus.SystemBus()
883 # Add a signal receiver for every path the client is interested in
884 for path in self.paths:
885 bus.add_signal_receiver(
886 self.interfaces_added_handler,
887 dbus_interface=dbus.BUS_DAEMON_IFACE + '.ObjectManager',
888 signal_name='InterfacesAdded',
889 path=path)
890 bus.add_signal_receiver(
891 self.properties_changed_handler,
892 dbus_interface=dbus.PROPERTIES_IFACE,
893 signal_name='PropertiesChanged',
894 path=path,
895 path_keyword='path')
896 loop = gobject.MainLoop()
897 # gobject's mainloop.run() will block the entire process, so the gevent
898 # scheduler and hence greenlets won't execute. The while-loop below
899 # works around this limitation by using gevent's sleep, instead of
900 # calling loop.run()
901 gcontext = loop.get_context()
902 while loop is not None:
903 try:
904 if gcontext.pending():
905 gcontext.iteration()
906 else:
907 # gevent.sleep puts only the current greenlet to sleep,
908 # not the entire process.
909 gevent.sleep(5)
910 except WebSocketError:
911 break
912
913 def interfaces_added_handler(self, path, iprops, **kw):
914 ''' If the client is interested in these changes, respond to the
915 client. This handles d-bus interface additions.'''
916 if (not self.interfaces) or \
917 (not set(iprops).isdisjoint(self.interfaces)):
918 response = {}
919 response[self.keyNames['event']] = "InterfacesAdded"
920 response[self.keyNames['path']] = path
921 response[self.keyNames['intfMap']] = iprops
922 try:
923 self.wsock.send(json.dumps(response))
924 except WebSocketError:
925 return
926
927 def properties_changed_handler(self, interface, new, old, **kw):
928 ''' If the client is interested in these changes, respond to the
929 client. This handles d-bus property changes. '''
930 if (not self.interfaces) or (interface in self.interfaces):
931 path = str(kw['path'])
932 response = {}
933 response[self.keyNames['event']] = "PropertiesChanged"
934 response[self.keyNames['path']] = path
935 response[self.keyNames['intf']] = interface
936 response[self.keyNames['propMap']] = new
937 try:
938 self.wsock.send(json.dumps(response))
939 except WebSocketError:
940 return
941
942
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500943class EventHandler(RouteHandler):
944 ''' Handles the /subscribe route, for clients to be able
945 to subscribe to BMC events. '''
946
947 verbs = ['GET']
948 rules = ['/subscribe']
949
950 def __init__(self, app, bus):
951 super(EventHandler, self).__init__(
952 app, bus, self.verbs, self.rules)
953
954 def find(self, **kw):
955 pass
956
957 def setup(self, **kw):
958 pass
959
960 def do_get(self):
961 wsock = request.environ.get('wsgi.websocket')
962 if not wsock:
963 abort(400, 'Expected WebSocket request.')
Jayashankar Padathbec10c22018-05-29 18:22:59 +0530964 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli639b5022017-10-13 06:40:26 -0500965 filters = wsock.receive()
966 filters = json.loads(filters)
967 notifier = EventNotifier(wsock, filters)
Deepak Kodihallib209dd12017-10-11 01:19:17 -0500968
Deepak Kodihalli5c518f62018-04-23 03:26:38 -0500969class HostConsoleHandler(RouteHandler):
970 ''' Handles the /console route, for clients to be able
971 read/write the host serial console. The way this is
972 done is by exposing a websocket that's mirrored to an
973 abstract UNIX domain socket, which is the source for
974 the console data. '''
975
976 verbs = ['GET']
977 # Naming the route console0, because the numbering will help
978 # on multi-bmc/multi-host systems.
979 rules = ['/console0']
980
981 def __init__(self, app, bus):
982 super(HostConsoleHandler, self).__init__(
983 app, bus, self.verbs, self.rules)
984
985 def find(self, **kw):
986 pass
987
988 def setup(self, **kw):
989 pass
990
991 def read_wsock(self, wsock, sock):
992 while True:
993 try:
994 incoming = wsock.receive()
995 if incoming:
996 # Read websocket, write to UNIX socket
997 sock.send(incoming)
998 except Exception as e:
999 sock.close()
1000 return
1001
1002 def read_sock(self, sock, wsock):
1003 max_sock_read_len = 4096
1004 while True:
1005 try:
1006 outgoing = sock.recv(max_sock_read_len)
1007 if outgoing:
1008 # Read UNIX socket, write to websocket
1009 wsock.send(outgoing)
1010 except Exception as e:
1011 wsock.close()
1012 return
1013
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001014 def do_get(self):
1015 wsock = request.environ.get('wsgi.websocket')
1016 if not wsock:
1017 abort(400, 'Expected WebSocket based request.')
1018
1019 # A UNIX domain socket structure defines a 108-byte pathname. The
1020 # server in this case, obmc-console-server, expects a 108-byte path.
1021 socket_name = "\0obmc-console"
1022 trailing_bytes = "\0" * (108 - len(socket_name))
1023 socket_path = socket_name + trailing_bytes
1024 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1025
1026 try:
1027 sock.connect(socket_path)
1028 except Exception as e:
1029 abort(500, str(e))
1030
1031 wsock_reader = Greenlet.spawn(self.read_wsock, wsock, sock)
1032 sock_reader = Greenlet.spawn(self.read_sock, sock, wsock)
Jayashankar Padathbec10c22018-05-29 18:22:59 +05301033 ping_sender = Greenlet.spawn(send_ws_ping, wsock, WEBSOCKET_TIMEOUT)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001034 gevent.joinall([wsock_reader, sock_reader, ping_sender])
1035
1036
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001037class ImagePutHandler(RouteHandler):
1038 ''' Handles the /upload/image/<filename> route. '''
1039
1040 verbs = ['PUT']
1041 rules = ['/upload/image/<filename>']
1042 content_type = 'application/octet-stream'
1043
1044 def __init__(self, app, bus):
1045 super(ImagePutHandler, self).__init__(
1046 app, bus, self.verbs, self.rules, self.content_type)
1047
1048 def do_put(self, filename=''):
Adriana Kobylak53693892018-03-12 13:05:50 -05001049 return ImageUploadUtils.do_upload(filename)
Deepak Kodihalli1af301a2017-04-11 07:29:01 -05001050
1051 def find(self, **kw):
1052 pass
1053
1054 def setup(self, **kw):
1055 pass
1056
1057
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001058class DownloadDumpHandler(RouteHandler):
1059 ''' Handles the /download/dump route. '''
1060
1061 verbs = 'GET'
1062 rules = ['/download/dump/<dumpid>']
1063 content_type = 'application/octet-stream'
Jayanth Othayoth18c3a242017-08-02 08:16:11 -05001064 dump_loc = '/var/lib/phosphor-debug-collector/dumps'
Brad Bishop944cd042017-07-10 16:42:41 -04001065 suppress_json_resp = True
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001066
1067 def __init__(self, app, bus):
1068 super(DownloadDumpHandler, self).__init__(
1069 app, bus, self.verbs, self.rules, self.content_type)
1070
1071 def do_get(self, dumpid):
1072 return self.do_download(dumpid)
1073
1074 def find(self, **kw):
1075 pass
1076
1077 def setup(self, **kw):
1078 pass
1079
1080 def do_download(self, dumpid):
1081 dump_loc = os.path.join(self.dump_loc, dumpid)
1082 if not os.path.exists(dump_loc):
1083 abort(404, "Path not found")
1084
1085 files = os.listdir(dump_loc)
1086 num_files = len(files)
1087 if num_files == 0:
1088 abort(404, "Dump not found")
1089
1090 return static_file(os.path.basename(files[0]), root=dump_loc,
1091 download=True, mimetype=self.content_type)
1092
1093
Matt Spinlerd41643e2018-02-02 13:51:38 -06001094class WebHandler(RouteHandler):
1095 ''' Handles the routes for the web UI files. '''
1096
1097 verbs = 'GET'
1098
1099 # Match only what we know are web files, so everything else
1100 # can get routed to the REST handlers.
1101 rules = ['//', '/<filename:re:.+\.js>', '/<filename:re:.+\.svg>',
1102 '/<filename:re:.+\.css>', '/<filename:re:.+\.ttf>',
1103 '/<filename:re:.+\.eot>', '/<filename:re:.+\.woff>',
1104 '/<filename:re:.+\.woff2>', '/<filename:re:.+\.map>',
1105 '/<filename:re:.+\.png>', '/<filename:re:.+\.html>',
1106 '/<filename:re:.+\.ico>']
1107
1108 # The mimetypes module knows about most types, but not these
1109 content_types = {
1110 '.eot': 'application/vnd.ms-fontobject',
1111 '.woff': 'application/x-font-woff',
1112 '.woff2': 'application/x-font-woff2',
1113 '.ttf': 'application/x-font-ttf',
1114 '.map': 'application/json'
1115 }
1116
1117 _require_auth = None
1118 suppress_json_resp = True
1119
1120 def __init__(self, app, bus):
1121 super(WebHandler, self).__init__(
1122 app, bus, self.verbs, self.rules)
1123
1124 def get_type(self, filename):
1125 ''' Returns the content type and encoding for a file '''
1126
1127 content_type, encoding = mimetypes.guess_type(filename)
1128
1129 # Try our own list if mimetypes didn't recognize it
1130 if content_type is None:
1131 if filename[-3:] == '.gz':
1132 filename = filename[:-3]
1133 extension = filename[filename.rfind('.'):]
1134 content_type = self.content_types.get(extension, None)
1135
1136 return content_type, encoding
1137
1138 def do_get(self, filename='index.html'):
1139
1140 # If a gzipped version exists, use that instead.
1141 # Possible future enhancement: if the client doesn't
1142 # accept compressed files, unzip it ourselves before sending.
1143 if not os.path.exists(os.path.join(www_base_path, filename)):
1144 filename = filename + '.gz'
1145
1146 # Though bottle should protect us, ensure path is valid
1147 realpath = os.path.realpath(filename)
1148 if realpath[0] == '/':
1149 realpath = realpath[1:]
1150 if not os.path.exists(os.path.join(www_base_path, realpath)):
1151 abort(404, "Path not found")
1152
1153 mimetype, encoding = self.get_type(filename)
1154
1155 # Couldn't find the type - let static_file() deal with it,
1156 # though this should never happen.
1157 if mimetype is None:
1158 print("Can't figure out content-type for %s" % filename)
1159 mimetype = 'auto'
1160
1161 # This call will set several header fields for us,
1162 # including the charset if the type is text.
1163 response = static_file(filename, www_base_path, mimetype)
1164
1165 # static_file() will only set the encoding if the
1166 # mimetype was auto, so set it here.
1167 if encoding is not None:
1168 response.set_header('Content-Encoding', encoding)
1169
1170 return response
1171
1172 def find(self, **kw):
1173 pass
1174
1175 def setup(self, **kw):
1176 pass
1177
1178
Brad Bishop2f428582015-12-02 10:56:11 -05001179class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001180 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -05001181
Brad Bishop87b63c12016-03-18 14:47:51 -04001182 name = 'authorization'
1183 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -05001184
Brad Bishop87b63c12016-03-18 14:47:51 -04001185 class Compose:
1186 def __init__(self, validators, callback, session_mgr):
1187 self.validators = validators
1188 self.callback = callback
1189 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -05001190
Brad Bishop87b63c12016-03-18 14:47:51 -04001191 def __call__(self, *a, **kw):
1192 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
1193 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -05001194 if request.method != 'OPTIONS':
1195 for x in self.validators:
1196 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001197
Brad Bishop87b63c12016-03-18 14:47:51 -04001198 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -05001199
Brad Bishop87b63c12016-03-18 14:47:51 -04001200 def apply(self, callback, route):
1201 undecorated = route.get_undecorated_callback()
1202 if not isinstance(undecorated, RouteHandler):
1203 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001204
Brad Bishop87b63c12016-03-18 14:47:51 -04001205 auth_types = getattr(
1206 undecorated, '_require_auth', None)
1207 if not auth_types:
1208 return callback
Brad Bishop2f428582015-12-02 10:56:11 -05001209
Brad Bishop87b63c12016-03-18 14:47:51 -04001210 return self.Compose(
1211 auth_types, callback, undecorated.app.session_handler)
1212
Brad Bishop2f428582015-12-02 10:56:11 -05001213
Brad Bishopd0c404a2017-02-21 09:23:25 -05001214class CorsPlugin(object):
1215 ''' Add CORS headers. '''
1216
1217 name = 'cors'
1218 api = 2
1219
1220 @staticmethod
1221 def process_origin():
1222 origin = request.headers.get('Origin')
1223 if origin:
1224 response.add_header('Access-Control-Allow-Origin', origin)
1225 response.add_header(
1226 'Access-Control-Allow-Credentials', 'true')
1227
1228 @staticmethod
1229 def process_method_and_headers(verbs):
1230 method = request.headers.get('Access-Control-Request-Method')
1231 headers = request.headers.get('Access-Control-Request-Headers')
1232 if headers:
1233 headers = [x.lower() for x in headers.split(',')]
1234
1235 if method in verbs \
1236 and headers == ['content-type']:
1237 response.add_header('Access-Control-Allow-Methods', method)
1238 response.add_header(
1239 'Access-Control-Allow-Headers', 'Content-Type')
Ratan Gupta91b46f82018-01-14 12:52:23 +05301240 response.add_header('X-Frame-Options', 'deny')
1241 response.add_header('X-Content-Type-Options', 'nosniff')
1242 response.add_header('X-XSS-Protection', '1; mode=block')
1243 response.add_header(
1244 'Content-Security-Policy', "default-src 'self'")
1245 response.add_header(
1246 'Strict-Transport-Security',
1247 'max-age=31536000; includeSubDomains; preload')
Brad Bishopd0c404a2017-02-21 09:23:25 -05001248
1249 def __init__(self, app):
1250 app.install_error_callback(self.error_callback)
1251
1252 def apply(self, callback, route):
1253 undecorated = route.get_undecorated_callback()
1254 if not isinstance(undecorated, RouteHandler):
1255 return callback
1256
1257 if not getattr(undecorated, '_enable_cors', None):
1258 return callback
1259
1260 def wrap(*a, **kw):
1261 self.process_origin()
1262 self.process_method_and_headers(undecorated._verbs)
1263 return callback(*a, **kw)
1264
1265 return wrap
1266
1267 def error_callback(self, **kw):
1268 self.process_origin()
1269
1270
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001271class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001272 ''' Ensures request content satisfies the OpenBMC json api format. '''
1273 name = 'json_api_request'
1274 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001275
Brad Bishop87b63c12016-03-18 14:47:51 -04001276 error_str = "Expecting request format { 'data': <value> }, got '%s'"
1277 type_error_str = "Unsupported Content-Type: '%s'"
1278 json_type = "application/json"
1279 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001280
Brad Bishop87b63c12016-03-18 14:47:51 -04001281 @staticmethod
1282 def content_expected():
1283 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001284
Brad Bishop87b63c12016-03-18 14:47:51 -04001285 def validate_request(self):
1286 if request.content_length > 0 and \
1287 request.content_type != self.json_type:
1288 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001289
Brad Bishop87b63c12016-03-18 14:47:51 -04001290 try:
1291 request.parameter_list = request.json.get('data')
CamVan Nguyen249d1322018-03-05 10:08:33 -06001292 except ValueError as e:
Brad Bishop87b63c12016-03-18 14:47:51 -04001293 abort(400, str(e))
1294 except (AttributeError, KeyError, TypeError):
1295 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001296
Brad Bishop87b63c12016-03-18 14:47:51 -04001297 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001298 content_type = getattr(
1299 route.get_undecorated_callback(), '_content_type', None)
1300 if self.json_type != content_type:
1301 return callback
1302
Brad Bishop87b63c12016-03-18 14:47:51 -04001303 verbs = getattr(
1304 route.get_undecorated_callback(), '_verbs', None)
1305 if verbs is None:
1306 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001307
Brad Bishop87b63c12016-03-18 14:47:51 -04001308 if not set(self.request_methods).intersection(verbs):
1309 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001310
Brad Bishop87b63c12016-03-18 14:47:51 -04001311 def wrap(*a, **kw):
1312 if self.content_expected():
1313 self.validate_request()
1314 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001315
Brad Bishop87b63c12016-03-18 14:47:51 -04001316 return wrap
1317
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001318
1319class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -04001320 ''' Ensures request content type satisfies the OpenBMC json api format. '''
1321 name = 'json_api_method_request'
1322 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001323
Brad Bishop87b63c12016-03-18 14:47:51 -04001324 error_str = "Expecting request format { 'data': %s }, got '%s'"
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001325 json_type = "application/json"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001326
Brad Bishop87b63c12016-03-18 14:47:51 -04001327 def apply(self, callback, route):
Deepak Kodihallifb6cd482017-04-10 07:27:09 -05001328 content_type = getattr(
1329 route.get_undecorated_callback(), '_content_type', None)
1330 if self.json_type != content_type:
1331 return callback
1332
Brad Bishop87b63c12016-03-18 14:47:51 -04001333 request_type = getattr(
1334 route.get_undecorated_callback(), 'request_type', None)
1335 if request_type is None:
1336 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001337
Brad Bishop87b63c12016-03-18 14:47:51 -04001338 def validate_request():
1339 if not isinstance(request.parameter_list, request_type):
1340 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001341
Brad Bishop87b63c12016-03-18 14:47:51 -04001342 def wrap(*a, **kw):
1343 if JsonApiRequestPlugin.content_expected():
1344 validate_request()
1345 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001346
Brad Bishop87b63c12016-03-18 14:47:51 -04001347 return wrap
1348
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001349
Brad Bishop080a48e2017-02-21 22:34:43 -05001350class JsonErrorsPlugin(JSONPlugin):
1351 ''' Extend the Bottle JSONPlugin such that it also encodes error
1352 responses. '''
1353
1354 def __init__(self, app, **kw):
1355 super(JsonErrorsPlugin, self).__init__(**kw)
1356 self.json_opts = {
CamVan Nguyen249d1322018-03-05 10:08:33 -06001357 x: y for x, y in kw.items()
Brad Bishop080a48e2017-02-21 22:34:43 -05001358 if x in ['indent', 'sort_keys']}
1359 app.install_error_callback(self.error_callback)
1360
1361 def error_callback(self, response_object, response_body, **kw):
1362 response_body['body'] = json.dumps(response_object, **self.json_opts)
1363 response.content_type = 'application/json'
1364
1365
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001366class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -05001367 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -04001368 name = 'json_api_response'
1369 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001370
Brad Bishopd4c1c552017-02-21 00:07:28 -05001371 @staticmethod
1372 def has_body():
1373 return request.method not in ['OPTIONS']
1374
Brad Bishop080a48e2017-02-21 22:34:43 -05001375 def __init__(self, app):
1376 app.install_error_callback(self.error_callback)
1377
Brad Bishop87b63c12016-03-18 14:47:51 -04001378 def apply(self, callback, route):
Brad Bishop944cd042017-07-10 16:42:41 -04001379 skip = getattr(
1380 route.get_undecorated_callback(), 'suppress_json_resp', None)
1381 if skip:
Jayanth Othayoth1444fd82017-06-29 05:45:07 -05001382 return callback
1383
Brad Bishop87b63c12016-03-18 14:47:51 -04001384 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -05001385 data = callback(*a, **kw)
1386 if self.has_body():
1387 resp = {'data': data}
1388 resp['status'] = 'ok'
1389 resp['message'] = response.status_line
1390 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -04001391 return wrap
1392
Brad Bishop080a48e2017-02-21 22:34:43 -05001393 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -04001394 response_object['message'] = error.status_line
Brad Bishop9c2531e2017-03-07 10:22:40 -05001395 response_object['status'] = 'error'
Brad Bishop080a48e2017-02-21 22:34:43 -05001396 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -04001397 if error.status_code == 500:
1398 response_object['data']['exception'] = repr(error.exception)
1399 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001400
Brad Bishop87b63c12016-03-18 14:47:51 -04001401
Brad Bishop080a48e2017-02-21 22:34:43 -05001402class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -04001403 ''' Json javascript wrapper. '''
1404 name = 'jsonp'
1405 api = 2
1406
Brad Bishop080a48e2017-02-21 22:34:43 -05001407 def __init__(self, app, **kw):
1408 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -04001409
1410 @staticmethod
1411 def to_jsonp(json):
1412 jwrapper = request.query.callback or None
1413 if(jwrapper):
1414 response.set_header('Content-Type', 'application/javascript')
1415 json = jwrapper + '(' + json + ');'
1416 return json
1417
1418 def apply(self, callback, route):
1419 def wrap(*a, **kw):
1420 return self.to_jsonp(callback(*a, **kw))
1421 return wrap
1422
Brad Bishop080a48e2017-02-21 22:34:43 -05001423 def error_callback(self, response_body, **kw):
1424 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -04001425
1426
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001427class ContentCheckerPlugin(object):
1428 ''' Ensures that a route is associated with the expected content-type
1429 header. '''
1430 name = 'content_checker'
1431 api = 2
1432
1433 class Checker:
1434 def __init__(self, type, callback):
1435 self.expected_type = type
1436 self.callback = callback
1437 self.error_str = "Expecting content type '%s', got '%s'"
1438
1439 def __call__(self, *a, **kw):
Deepak Kodihallidb1a21e2017-04-27 06:30:11 -05001440 if request.method in ['PUT', 'POST', 'PATCH'] and \
1441 self.expected_type and \
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001442 self.expected_type != request.content_type:
1443 abort(415, self.error_str % (self.expected_type,
1444 request.content_type))
1445
1446 return self.callback(*a, **kw)
1447
1448 def apply(self, callback, route):
1449 content_type = getattr(
1450 route.get_undecorated_callback(), '_content_type', None)
1451
1452 return self.Checker(content_type, callback)
1453
Nagaraju Goruganti0cf702c2018-04-17 22:27:08 -05001454class CheckURLPlugin(object):
1455 ''' Ensures that anything read and written using only urls listed in
1456 the url_config.json config file would allowed. '''
1457 name = 'url_checker'
1458 api = 2
1459
1460 def __init__(self):
1461 config_path = '/usr/share/rest-dbus/url_config.json'
1462 url_config = {}
1463 urls = {}
1464 self.pattern = {}
1465 if os.path.exists(config_path):
1466 try:
1467 with open(config_path) as data_file:
1468 url_config = json.load(data_file)
1469 urls = url_config.get("urls", ["*"])
1470 self.pattern = '|'.join(fnmatch.translate(p) for p in urls)
1471 self.pattern = re.compile(self.pattern)
1472 except ValueError as e:
1473 abort(404, str(e))
1474 else:
1475 abort(404, "Config file path not found for Whitelisted URLs")
1476
1477 def apply(self, callback, route):
1478
1479 def wrap(*a, **kw):
1480 if self.pattern.match(request.path):
1481 return callback(*a, **kw)
1482 abort(404,"Trying to access Blocked URL")
1483 return wrap
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001484
Brad Bishop2c6fc762016-08-29 15:53:25 -04001485class App(Bottle):
Deepak Kodihalli0fe213f2017-10-11 00:08:48 -05001486 def __init__(self, **kw):
Brad Bishop2c6fc762016-08-29 15:53:25 -04001487 super(App, self).__init__(autojson=False)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001488
1489 self.have_wsock = kw.get('have_wsock', False)
Alexander Filippovd08a4562018-03-20 12:02:23 +03001490 self.with_bmc_check = '--with-bmc-check' in sys.argv
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001491
Brad Bishop2ddfa002016-08-29 15:11:55 -04001492 self.bus = dbus.SystemBus()
1493 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -05001494 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001495
Brad Bishop87b63c12016-03-18 14:47:51 -04001496 self.install_hooks()
1497 self.install_plugins()
1498 self.create_handlers()
1499 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001500
Brad Bishop87b63c12016-03-18 14:47:51 -04001501 def install_plugins(self):
1502 # install json api plugins
1503 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -04001504 self.install(AuthorizationPlugin())
Brad Bishopd0c404a2017-02-21 09:23:25 -05001505 self.install(CorsPlugin(self))
Deepak Kodihalli461367a2017-04-10 07:11:38 -05001506 self.install(ContentCheckerPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -05001507 self.install(JsonpPlugin(self, **json_kw))
1508 self.install(JsonErrorsPlugin(self, **json_kw))
1509 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -04001510 self.install(JsonApiRequestPlugin())
1511 self.install(JsonApiRequestTypePlugin())
Nagaraju Goruganti0cf702c2018-04-17 22:27:08 -05001512 self.install(CheckURLPlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001513
Brad Bishop87b63c12016-03-18 14:47:51 -04001514 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -05001515 self.error_handler_type = type(self.default_error_handler)
1516 self.original_error_handler = self.default_error_handler
1517 self.default_error_handler = self.error_handler_type(
1518 self.custom_error_handler, self, Bottle)
1519
Brad Bishop87b63c12016-03-18 14:47:51 -04001520 self.real_router_match = self.router.match
1521 self.router.match = self.custom_router_match
1522 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001523
Brad Bishop87b63c12016-03-18 14:47:51 -04001524 def create_handlers(self):
1525 # create route handlers
1526 self.session_handler = SessionHandler(self, self.bus)
Matt Spinlerd41643e2018-02-02 13:51:38 -06001527 self.web_handler = WebHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001528 self.directory_handler = DirectoryHandler(self, self.bus)
1529 self.list_names_handler = ListNamesHandler(self, self.bus)
1530 self.list_handler = ListHandler(self, self.bus)
1531 self.method_handler = MethodHandler(self, self.bus)
1532 self.property_handler = PropertyHandler(self, self.bus)
1533 self.schema_handler = SchemaHandler(self, self.bus)
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001534 self.image_upload_post_handler = ImagePostHandler(self, self.bus)
1535 self.image_upload_put_handler = ImagePutHandler(self, self.bus)
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001536 self.download_dump_get_handler = DownloadDumpHandler(self, self.bus)
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001537 if self.have_wsock:
1538 self.event_handler = EventHandler(self, self.bus)
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001539 self.host_console_handler = HostConsoleHandler(self, self.bus)
Brad Bishop87b63c12016-03-18 14:47:51 -04001540 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001541
Brad Bishop87b63c12016-03-18 14:47:51 -04001542 def install_handlers(self):
1543 self.session_handler.install()
Matt Spinlerd41643e2018-02-02 13:51:38 -06001544 self.web_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001545 self.directory_handler.install()
1546 self.list_names_handler.install()
1547 self.list_handler.install()
1548 self.method_handler.install()
1549 self.property_handler.install()
1550 self.schema_handler.install()
Deepak Kodihalli7ec0a4f2017-04-11 07:50:27 -05001551 self.image_upload_post_handler.install()
1552 self.image_upload_put_handler.install()
Jayanth Othayoth9bc94992017-06-29 06:30:40 -05001553 self.download_dump_get_handler.install()
Deepak Kodihallib209dd12017-10-11 01:19:17 -05001554 if self.have_wsock:
1555 self.event_handler.install()
Deepak Kodihalli5c518f62018-04-23 03:26:38 -05001556 self.host_console_handler.install()
Brad Bishop87b63c12016-03-18 14:47:51 -04001557 # this has to come last, since it matches everything
1558 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001559
Brad Bishop080a48e2017-02-21 22:34:43 -05001560 def install_error_callback(self, callback):
1561 self.error_callbacks.insert(0, callback)
1562
Brad Bishop87b63c12016-03-18 14:47:51 -04001563 def custom_router_match(self, environ):
1564 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
1565 needed doesn't work for us since the instance rules match
1566 everything. This monkey-patch lets the route handler figure
1567 out which response is needed. This could be accomplished
1568 with a hook but that would require calling the router match
1569 function twice.
1570 '''
1571 route, args = self.real_router_match(environ)
1572 if isinstance(route.callback, RouteHandler):
1573 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001574
Brad Bishop87b63c12016-03-18 14:47:51 -04001575 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05001576
Brad Bishop080a48e2017-02-21 22:34:43 -05001577 def custom_error_handler(self, res, error):
Gunnar Millsf01d0ba2017-10-25 20:37:24 -05001578 ''' Allow plugins to modify error responses too via this custom
Brad Bishop080a48e2017-02-21 22:34:43 -05001579 error handler. '''
1580
1581 response_object = {}
1582 response_body = {}
1583 for x in self.error_callbacks:
1584 x(error=error,
1585 response_object=response_object,
1586 response_body=response_body)
1587
1588 return response_body.get('body', "")
1589
Brad Bishop87b63c12016-03-18 14:47:51 -04001590 @staticmethod
1591 def strip_extra_slashes():
1592 path = request.environ['PATH_INFO']
1593 trailing = ("", "/")[path[-1] == '/']
CamVan Nguyen249d1322018-03-05 10:08:33 -06001594 parts = list(filter(bool, path.split('/')))
Brad Bishop87b63c12016-03-18 14:47:51 -04001595 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing