|
|
@@ -17,126 +17,22 @@ import decorator |
|
|
|
import psutil |
|
|
|
import traceback |
|
|
|
|
|
|
|
from nilmdb.server.serverutil import ( |
|
|
|
chunked_response, |
|
|
|
response_type, |
|
|
|
workaround_cp_bug_1200, |
|
|
|
exception_to_httperror, |
|
|
|
CORS_allow, |
|
|
|
json_to_request_params, |
|
|
|
) |
|
|
|
|
|
|
|
# Add CORS_allow tool |
|
|
|
cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) |
|
|
|
|
|
|
|
class NilmApp(object): |
|
|
|
def __init__(self, db): |
|
|
|
self.db = db |
|
|
|
|
|
|
|
# Decorators |
|
|
|
def chunked_response(func): |
|
|
|
"""Decorator to enable chunked responses.""" |
|
|
|
# Set this to False to get better tracebacks from some requests |
|
|
|
# (/stream/extract, /stream/intervals). |
|
|
|
func._cp_config = { 'response.stream': True } |
|
|
|
return func |
|
|
|
|
|
|
|
def response_type(content_type): |
|
|
|
"""Return a decorator-generating function that sets the |
|
|
|
response type to the specified string.""" |
|
|
|
def wrapper(func, *args, **kwargs): |
|
|
|
cherrypy.response.headers['Content-Type'] = content_type |
|
|
|
return func(*args, **kwargs) |
|
|
|
return decorator.decorator(wrapper) |
|
|
|
|
|
|
|
@decorator.decorator |
|
|
|
def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover |
|
|
|
"""Decorator to work around CherryPy bug #1200 in a response |
|
|
|
generator. |
|
|
|
|
|
|
|
Even if chunked responses are disabled, LookupError or |
|
|
|
UnicodeError exceptions may still be swallowed by CherryPy due to |
|
|
|
bug #1200. This throws them as generic Exceptions instead so that |
|
|
|
they make it through. |
|
|
|
""" |
|
|
|
exc_info = None |
|
|
|
try: |
|
|
|
for val in func(*args, **kwargs): |
|
|
|
yield val |
|
|
|
except (LookupError, UnicodeError): |
|
|
|
# Re-raise it, but maintain the original traceback |
|
|
|
exc_info = sys.exc_info() |
|
|
|
new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1])) |
|
|
|
raise new_exc, None, exc_info[2] |
|
|
|
finally: |
|
|
|
del exc_info |
|
|
|
|
|
|
|
def exception_to_httperror(*expected): |
|
|
|
"""Return a decorator-generating function that catches expected |
|
|
|
errors and throws a HTTPError describing it instead. |
|
|
|
|
|
|
|
@exception_to_httperror(NilmDBError, ValueError) |
|
|
|
def foo(): |
|
|
|
pass |
|
|
|
""" |
|
|
|
def wrapper(func, *args, **kwargs): |
|
|
|
exc_info = None |
|
|
|
try: |
|
|
|
return func(*args, **kwargs) |
|
|
|
except expected: |
|
|
|
# Re-raise it, but maintain the original traceback |
|
|
|
exc_info = sys.exc_info() |
|
|
|
new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1])) |
|
|
|
raise new_exc, None, exc_info[2] |
|
|
|
finally: |
|
|
|
del exc_info |
|
|
|
# We need to preserve the function's argspecs for CherryPy to |
|
|
|
# handle argument errors correctly. Decorator.decorator takes |
|
|
|
# care of that. |
|
|
|
return decorator.decorator(wrapper) |
|
|
|
|
|
|
|
# Custom CherryPy tools |
|
|
|
|
|
|
|
def CORS_allow(methods): |
|
|
|
"""This does several things: |
|
|
|
|
|
|
|
Handles CORS preflight requests. |
|
|
|
Adds Allow: header to all requests. |
|
|
|
Raise 405 if request.method not in method. |
|
|
|
|
|
|
|
It is similar to cherrypy.tools.allow, with the CORS stuff added. |
|
|
|
""" |
|
|
|
request = cherrypy.request.headers |
|
|
|
response = cherrypy.response.headers |
|
|
|
|
|
|
|
if not isinstance(methods, (tuple, list)): # pragma: no cover |
|
|
|
methods = [ methods ] |
|
|
|
methods = [ m.upper() for m in methods if m ] |
|
|
|
if not methods: # pragma: no cover |
|
|
|
methods = [ 'GET', 'HEAD' ] |
|
|
|
elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover |
|
|
|
methods.append('HEAD') |
|
|
|
response['Allow'] = ', '.join(methods) |
|
|
|
|
|
|
|
# Allow all origins |
|
|
|
if 'Origin' in request: |
|
|
|
response['Access-Control-Allow-Origin'] = request['Origin'] |
|
|
|
|
|
|
|
# If it's a CORS request, send response. |
|
|
|
request_method = request.get("Access-Control-Request-Method", None) |
|
|
|
request_headers = request.get("Access-Control-Request-Headers", None) |
|
|
|
if (cherrypy.request.method == "OPTIONS" and |
|
|
|
request_method and request_headers): |
|
|
|
response['Access-Control-Allow-Headers'] = request_headers |
|
|
|
response['Access-Control-Allow-Methods'] = ', '.join(methods) |
|
|
|
# Try to stop further processing and return a 200 OK |
|
|
|
cherrypy.response.status = "200 OK" |
|
|
|
cherrypy.response.body = "" |
|
|
|
cherrypy.request.handler = lambda: "" |
|
|
|
return |
|
|
|
|
|
|
|
# Reject methods that were not explicitly allowed |
|
|
|
if cherrypy.request.method not in methods: |
|
|
|
raise cherrypy.HTTPError(405) |
|
|
|
|
|
|
|
cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow) |
|
|
|
|
|
|
|
# Helper for json_in tool to process JSON data into normal request |
|
|
|
# parameters. |
|
|
|
def json_to_request_params(body): |
|
|
|
cherrypy.lib.jsontools.json_processor(body) |
|
|
|
if not isinstance(cherrypy.request.json, dict): |
|
|
|
raise cherrypy.HTTPError(415) |
|
|
|
cherrypy.request.params.update(cherrypy.request.json) |
|
|
|
|
|
|
|
# CherryPy apps |
|
|
|
class Root(NilmApp): |
|
|
|
"""Root application for NILM database""" |
|
|
|