You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

190 lines
6.7 KiB

  1. """Miscellaneous decorators and other helpers for running a CherryPy
  2. server"""
  3. import cherrypy
  4. import sys
  5. import os
  6. import decorator
  7. import json
  8. # Helper to parse parameters into booleans
  9. def bool_param(s):
  10. """Return a bool indicating whether parameter 's' was True or False,
  11. supporting a few different types for 's'."""
  12. try:
  13. ss = s.lower()
  14. if ss in [ "0", "false", "f", "no", "n" ]:
  15. return False
  16. if ss in [ "1", "true", "t", "yes", "y" ]:
  17. return True
  18. except Exception:
  19. return bool(s)
  20. raise cherrypy.HTTPError("400 Bad Request",
  21. "can't parse parameter: " + ss)
  22. # Decorators
  23. def chunked_response(func):
  24. """Decorator to enable chunked responses."""
  25. # Set this to False to get better tracebacks from some requests
  26. # (/stream/extract, /stream/intervals).
  27. func._cp_config = { 'response.stream': True }
  28. return func
  29. def response_type(content_type):
  30. """Return a decorator-generating function that sets the
  31. response type to the specified string."""
  32. def wrapper(func, *args, **kwargs):
  33. cherrypy.response.headers['Content-Type'] = content_type
  34. return func(*args, **kwargs)
  35. return decorator.decorator(wrapper)
  36. def exception_to_httperror(*expected):
  37. """Return a decorator-generating function that catches expected
  38. errors and throws a HTTPError describing it instead.
  39. @exception_to_httperror(NilmDBError, ValueError)
  40. def foo():
  41. pass
  42. """
  43. def wrapper(func, *args, **kwargs):
  44. exc_info = None
  45. try:
  46. return func(*args, **kwargs)
  47. except expected:
  48. # Re-raise it, but maintain the original traceback
  49. exc_info = sys.exc_info()
  50. new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1]))
  51. raise new_exc.with_traceback(exc_info[2])
  52. finally:
  53. del exc_info
  54. # We need to preserve the function's argspecs for CherryPy to
  55. # handle argument errors correctly. Decorator.decorator takes
  56. # care of that.
  57. return decorator.decorator(wrapper)
  58. # Custom CherryPy tools
  59. def CORS_allow(methods):
  60. """This does several things:
  61. Handles CORS preflight requests.
  62. Adds Allow: header to all requests.
  63. Raise 405 if request.method not in method.
  64. It is similar to cherrypy.tools.allow, with the CORS stuff added.
  65. Add this to CherryPy with:
  66. cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
  67. """
  68. request = cherrypy.request.headers
  69. response = cherrypy.response.headers
  70. if not isinstance(methods, (tuple, list)):
  71. methods = [ methods ]
  72. methods = [ m.upper() for m in methods if m ]
  73. if not methods:
  74. methods = [ 'GET', 'HEAD' ]
  75. elif 'GET' in methods and 'HEAD' not in methods:
  76. methods.append('HEAD')
  77. response['Allow'] = ', '.join(methods)
  78. # Allow all origins
  79. if 'Origin' in request:
  80. response['Access-Control-Allow-Origin'] = request['Origin']
  81. # If it's a CORS request, send response.
  82. request_method = request.get("Access-Control-Request-Method", None)
  83. request_headers = request.get("Access-Control-Request-Headers", None)
  84. if (cherrypy.request.method == "OPTIONS" and
  85. request_method and request_headers):
  86. response['Access-Control-Allow-Headers'] = request_headers
  87. response['Access-Control-Allow-Methods'] = ', '.join(methods)
  88. # Try to stop further processing and return a 200 OK
  89. cherrypy.response.status = "200 OK"
  90. cherrypy.response.body = b""
  91. cherrypy.request.handler = lambda: ""
  92. return
  93. # Reject methods that were not explicitly allowed
  94. if cherrypy.request.method not in methods:
  95. raise cherrypy.HTTPError(405)
  96. # Helper for json_in tool to process JSON data into normal request
  97. # parameters.
  98. def json_to_request_params(body):
  99. cherrypy.lib.jsontools.json_processor(body)
  100. if not isinstance(cherrypy.request.json, dict):
  101. raise cherrypy.HTTPError(415)
  102. cherrypy.request.params.update(cherrypy.request.json)
  103. # Used as an "error_page.default" handler
  104. def json_error_page(status, message, traceback, version,
  105. force_traceback = False):
  106. """Return a custom error page in JSON so the client can parse it"""
  107. errordata = { "status" : status,
  108. "message" : message,
  109. "traceback" : traceback }
  110. # Don't send a traceback if the error was 400-499 (client's fault)
  111. code = int(status.split()[0])
  112. if not force_traceback:
  113. if code >= 400 and code <= 499:
  114. errordata["traceback"] = ""
  115. # Override the response type, which was previously set to text/html
  116. cherrypy.serving.response.headers['Content-Type'] = (
  117. "application/json;charset=utf-8" )
  118. # Undo the HTML escaping that cherrypy's get_error_page function applies
  119. # (cherrypy issue 1135)
  120. for k, v in errordata.items():
  121. v = v.replace("&lt;","<")
  122. v = v.replace("&gt;",">")
  123. v = v.replace("&amp;","&")
  124. errordata[k] = v
  125. return json.dumps(errordata, separators=(',',':'))
  126. # Start/stop CherryPy standalone server
  127. def cherrypy_start(blocking = False, event = False, embedded = False):
  128. """Start the CherryPy server, handling errors and signals
  129. somewhat gracefully."""
  130. if not embedded: # pragma: no cover
  131. # Handle signals nicely
  132. if hasattr(cherrypy.engine, "signal_handler"):
  133. cherrypy.engine.signal_handler.subscribe()
  134. if hasattr(cherrypy.engine, "console_control_handler"):
  135. cherrypy.engine.console_control_handler.subscribe()
  136. # Cherrypy stupidly calls os._exit(70) when it can't bind the
  137. # port. At least try to print a reasonable error and continue
  138. # in this case, rather than just dying silently (as we would
  139. # otherwise do in embedded mode)
  140. real_exit = os._exit
  141. def fake_exit(code): # pragma: no cover
  142. if code == os.EX_SOFTWARE:
  143. fprintf(sys.stderr, "error: CherryPy called os._exit!\n")
  144. else:
  145. real_exit(code)
  146. os._exit = fake_exit
  147. cherrypy.engine.start()
  148. os._exit = real_exit
  149. # Signal that the engine has started successfully
  150. if event is not None:
  151. event.set()
  152. if blocking:
  153. try:
  154. cherrypy.engine.wait(cherrypy.engine.states.EXITING,
  155. interval = 0.1, channel = 'main')
  156. except (KeyboardInterrupt, IOError): # pragma: no cover
  157. cherrypy.engine.log('Keyboard Interrupt: shutting down bus')
  158. cherrypy.engine.exit()
  159. except SystemExit: # pragma: no cover
  160. cherrypy.engine.log('SystemExit raised: shutting down bus')
  161. cherrypy.engine.exit()
  162. raise
  163. # Stop CherryPy server
  164. def cherrypy_stop():
  165. cherrypy.engine.exit()