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.
 
 
 

215 lines
7.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 simplejson as 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. @decorator.decorator
  37. def workaround_cp_bug_1200(func, *args, **kwargs): # pragma: no cover
  38. """Decorator to work around CherryPy bug #1200 in a response
  39. generator.
  40. Even if chunked responses are disabled, LookupError or
  41. UnicodeError exceptions may still be swallowed by CherryPy due to
  42. bug #1200. This throws them as generic Exceptions instead so that
  43. they make it through.
  44. """
  45. exc_info = None
  46. try:
  47. for val in func(*args, **kwargs):
  48. yield val
  49. except (LookupError, UnicodeError):
  50. # Re-raise it, but maintain the original traceback
  51. exc_info = sys.exc_info()
  52. new_exc = Exception(exc_info[0].__name__ + ": " + str(exc_info[1]))
  53. raise new_exc.with_traceback(exc_info[2])
  54. finally:
  55. del exc_info
  56. def exception_to_httperror(*expected):
  57. """Return a decorator-generating function that catches expected
  58. errors and throws a HTTPError describing it instead.
  59. @exception_to_httperror(NilmDBError, ValueError)
  60. def foo():
  61. pass
  62. """
  63. def wrapper(func, *args, **kwargs):
  64. exc_info = None
  65. try:
  66. return func(*args, **kwargs)
  67. except expected:
  68. # Re-raise it, but maintain the original traceback
  69. exc_info = sys.exc_info()
  70. new_exc = cherrypy.HTTPError("400 Bad Request", str(exc_info[1]))
  71. raise new_exc.with_traceback(exc_info[2])
  72. finally:
  73. del exc_info
  74. # We need to preserve the function's argspecs for CherryPy to
  75. # handle argument errors correctly. Decorator.decorator takes
  76. # care of that.
  77. return decorator.decorator(wrapper)
  78. # Custom CherryPy tools
  79. def CORS_allow(methods):
  80. """This does several things:
  81. Handles CORS preflight requests.
  82. Adds Allow: header to all requests.
  83. Raise 405 if request.method not in method.
  84. It is similar to cherrypy.tools.allow, with the CORS stuff added.
  85. Add this to CherryPy with:
  86. cherrypy.tools.CORS_allow = cherrypy.Tool('on_start_resource', CORS_allow)
  87. """
  88. request = cherrypy.request.headers
  89. response = cherrypy.response.headers
  90. if not isinstance(methods, (tuple, list)): # pragma: no cover
  91. methods = [ methods ]
  92. methods = [ m.upper() for m in methods if m ]
  93. if not methods: # pragma: no cover
  94. methods = [ 'GET', 'HEAD' ]
  95. elif 'GET' in methods and 'HEAD' not in methods: # pragma: no cover
  96. methods.append('HEAD')
  97. response['Allow'] = ', '.join(methods)
  98. # Allow all origins
  99. if 'Origin' in request:
  100. response['Access-Control-Allow-Origin'] = request['Origin']
  101. # If it's a CORS request, send response.
  102. request_method = request.get("Access-Control-Request-Method", None)
  103. request_headers = request.get("Access-Control-Request-Headers", None)
  104. if (cherrypy.request.method == "OPTIONS" and
  105. request_method and request_headers):
  106. response['Access-Control-Allow-Headers'] = request_headers
  107. response['Access-Control-Allow-Methods'] = ', '.join(methods)
  108. # Try to stop further processing and return a 200 OK
  109. cherrypy.response.status = "200 OK"
  110. cherrypy.response.body = b""
  111. cherrypy.request.handler = lambda: ""
  112. return
  113. # Reject methods that were not explicitly allowed
  114. if cherrypy.request.method not in methods:
  115. raise cherrypy.HTTPError(405)
  116. # Helper for json_in tool to process JSON data into normal request
  117. # parameters.
  118. def json_to_request_params(body):
  119. cherrypy.lib.jsontools.json_processor(body)
  120. if not isinstance(cherrypy.request.json, dict):
  121. raise cherrypy.HTTPError(415)
  122. cherrypy.request.params.update(cherrypy.request.json)
  123. # Used as an "error_page.default" handler
  124. def json_error_page(status, message, traceback, version,
  125. force_traceback = False):
  126. """Return a custom error page in JSON so the client can parse it"""
  127. errordata = { "status" : status,
  128. "message" : message,
  129. "traceback" : traceback }
  130. # Don't send a traceback if the error was 400-499 (client's fault)
  131. try:
  132. code = int(status.split()[0])
  133. if not force_traceback:
  134. if code >= 400 and code <= 499:
  135. errordata["traceback"] = ""
  136. except Exception: # pragma: no cover
  137. pass
  138. # Override the response type, which was previously set to text/html
  139. cherrypy.serving.response.headers['Content-Type'] = (
  140. "application/json;charset=utf-8" )
  141. # Undo the HTML escaping that cherrypy's get_error_page function applies
  142. # (cherrypy issue 1135)
  143. for k, v in errordata.items():
  144. v = v.replace("&lt;","<")
  145. v = v.replace("&gt;",">")
  146. v = v.replace("&amp;","&")
  147. errordata[k] = v
  148. return json.dumps(errordata, separators=(',',':'))
  149. # Start/stop CherryPy standalone server
  150. def cherrypy_start(blocking = False, event = False, embedded = False):
  151. """Start the CherryPy server, handling errors and signals
  152. somewhat gracefully."""
  153. if not embedded: # pragma: no cover
  154. # Handle signals nicely
  155. if hasattr(cherrypy.engine, "signal_handler"):
  156. cherrypy.engine.signal_handler.subscribe()
  157. if hasattr(cherrypy.engine, "console_control_handler"):
  158. cherrypy.engine.console_control_handler.subscribe()
  159. # Cherrypy stupidly calls os._exit(70) when it can't bind the
  160. # port. At least try to print a reasonable error and continue
  161. # in this case, rather than just dying silently (as we would
  162. # otherwise do in embedded mode)
  163. real_exit = os._exit
  164. def fake_exit(code): # pragma: no cover
  165. if code == os.EX_SOFTWARE:
  166. fprintf(sys.stderr, "error: CherryPy called os._exit!\n")
  167. else:
  168. real_exit(code)
  169. os._exit = fake_exit
  170. cherrypy.engine.start()
  171. os._exit = real_exit
  172. # Signal that the engine has started successfully
  173. if event is not None:
  174. event.set()
  175. if blocking:
  176. try:
  177. cherrypy.engine.wait(cherrypy.engine.states.EXITING,
  178. interval = 0.1, channel = 'main')
  179. except (KeyboardInterrupt, IOError): # pragma: no cover
  180. cherrypy.engine.log('Keyboard Interrupt: shutting down bus')
  181. cherrypy.engine.exit()
  182. except SystemExit: # pragma: no cover
  183. cherrypy.engine.log('SystemExit raised: shutting down bus')
  184. cherrypy.engine.exit()
  185. raise
  186. # Stop CherryPy server
  187. def cherrypy_stop():
  188. cherrypy.engine.exit()