recursive.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3. """
  4. Middleware to make internal requests and forward requests internally.
  5. When applied, several keys are added to the environment that will allow
  6. you to trigger recursive redirects and forwards.
  7. paste.recursive.include:
  8. When you call
  9. ``environ['paste.recursive.include'](new_path_info)`` a response
  10. will be returned. The response has a ``body`` attribute, a
  11. ``status`` attribute, and a ``headers`` attribute.
  12. paste.recursive.script_name:
  13. The ``SCRIPT_NAME`` at the point that recursive lives. Only
  14. paths underneath this path can be redirected to.
  15. paste.recursive.old_path_info:
  16. A list of previous ``PATH_INFO`` values from previous redirects.
  17. Raise ``ForwardRequestException(new_path_info)`` to do a forward
  18. (aborting the current request).
  19. """
  20. import six
  21. import warnings
  22. from six.moves import cStringIO as StringIO
  23. __all__ = ['RecursiveMiddleware']
  24. __pudge_all__ = ['RecursiveMiddleware', 'ForwardRequestException']
  25. class RecursionLoop(AssertionError):
  26. # Subclasses AssertionError for legacy reasons
  27. """Raised when a recursion enters into a loop"""
  28. class CheckForRecursionMiddleware(object):
  29. def __init__(self, app, env):
  30. self.app = app
  31. self.env = env
  32. def __call__(self, environ, start_response):
  33. path_info = environ.get('PATH_INFO','')
  34. if path_info in self.env.get(
  35. 'paste.recursive.old_path_info', []):
  36. raise RecursionLoop(
  37. "Forwarding loop detected; %r visited twice (internal "
  38. "redirect path: %s)"
  39. % (path_info, self.env['paste.recursive.old_path_info']))
  40. old_path_info = self.env.setdefault('paste.recursive.old_path_info', [])
  41. old_path_info.append(self.env.get('PATH_INFO', ''))
  42. return self.app(environ, start_response)
  43. class RecursiveMiddleware(object):
  44. """
  45. A WSGI middleware that allows for recursive and forwarded calls.
  46. All these calls go to the same 'application', but presumably that
  47. application acts differently with different URLs. The forwarded
  48. URLs must be relative to this container.
  49. Interface is entirely through the ``paste.recursive.forward`` and
  50. ``paste.recursive.include`` environmental keys.
  51. """
  52. def __init__(self, application, global_conf=None):
  53. self.application = application
  54. def __call__(self, environ, start_response):
  55. environ['paste.recursive.forward'] = Forwarder(
  56. self.application,
  57. environ,
  58. start_response)
  59. environ['paste.recursive.include'] = Includer(
  60. self.application,
  61. environ,
  62. start_response)
  63. environ['paste.recursive.include_app_iter'] = IncluderAppIter(
  64. self.application,
  65. environ,
  66. start_response)
  67. my_script_name = environ.get('SCRIPT_NAME', '')
  68. environ['paste.recursive.script_name'] = my_script_name
  69. try:
  70. return self.application(environ, start_response)
  71. except ForwardRequestException as e:
  72. middleware = CheckForRecursionMiddleware(
  73. e.factory(self), environ)
  74. return middleware(environ, start_response)
  75. class ForwardRequestException(Exception):
  76. """
  77. Used to signal that a request should be forwarded to a different location.
  78. ``url``
  79. The URL to forward to starting with a ``/`` and relative to
  80. ``RecursiveMiddleware``. URL fragments can also contain query strings
  81. so ``/error?code=404`` would be a valid URL fragment.
  82. ``environ``
  83. An altertative WSGI environment dictionary to use for the forwarded
  84. request. If specified is used *instead* of the ``url_fragment``
  85. ``factory``
  86. If specifed ``factory`` is used instead of ``url`` or ``environ``.
  87. ``factory`` is a callable that takes a WSGI application object
  88. as the first argument and returns an initialised WSGI middleware
  89. which can alter the forwarded response.
  90. Basic usage (must have ``RecursiveMiddleware`` present) :
  91. .. code-block:: python
  92. from paste.recursive import ForwardRequestException
  93. def app(environ, start_response):
  94. if environ['PATH_INFO'] == '/hello':
  95. start_response("200 OK", [('Content-type', 'text/plain')])
  96. return [b'Hello World!']
  97. elif environ['PATH_INFO'] == '/error':
  98. start_response("404 Not Found", [('Content-type', 'text/plain')])
  99. return [b'Page not found']
  100. else:
  101. raise ForwardRequestException('/error')
  102. from paste.recursive import RecursiveMiddleware
  103. app = RecursiveMiddleware(app)
  104. If you ran this application and visited ``/hello`` you would get a
  105. ``Hello World!`` message. If you ran the application and visited
  106. ``/not_found`` a ``ForwardRequestException`` would be raised and the caught
  107. by the ``RecursiveMiddleware``. The ``RecursiveMiddleware`` would then
  108. return the headers and response from the ``/error`` URL but would display
  109. a ``404 Not found`` status message.
  110. You could also specify an ``environ`` dictionary instead of a url. Using
  111. the same example as before:
  112. .. code-block:: python
  113. def app(environ, start_response):
  114. ... same as previous example ...
  115. else:
  116. new_environ = environ.copy()
  117. new_environ['PATH_INFO'] = '/error'
  118. raise ForwardRequestException(environ=new_environ)
  119. Finally, if you want complete control over every aspect of the forward you
  120. can specify a middleware factory. For example to keep the old status code
  121. but use the headers and resposne body from the forwarded response you might
  122. do this:
  123. .. code-block:: python
  124. from paste.recursive import ForwardRequestException
  125. from paste.recursive import RecursiveMiddleware
  126. from paste.errordocument import StatusKeeper
  127. def app(environ, start_response):
  128. if environ['PATH_INFO'] == '/hello':
  129. start_response("200 OK", [('Content-type', 'text/plain')])
  130. return [b'Hello World!']
  131. elif environ['PATH_INFO'] == '/error':
  132. start_response("404 Not Found", [('Content-type', 'text/plain')])
  133. return [b'Page not found']
  134. else:
  135. def factory(app):
  136. return StatusKeeper(app, status='404 Not Found', url='/error')
  137. raise ForwardRequestException(factory=factory)
  138. app = RecursiveMiddleware(app)
  139. """
  140. def __init__(
  141. self,
  142. url=None,
  143. environ={},
  144. factory=None,
  145. path_info=None):
  146. # Check no incompatible options have been chosen
  147. if factory and url:
  148. raise TypeError(
  149. 'You cannot specify factory and a url in '
  150. 'ForwardRequestException')
  151. elif factory and environ:
  152. raise TypeError(
  153. 'You cannot specify factory and environ in '
  154. 'ForwardRequestException')
  155. if url and environ:
  156. raise TypeError(
  157. 'You cannot specify environ and url in '
  158. 'ForwardRequestException')
  159. # set the path_info or warn about its use.
  160. if path_info:
  161. if not url:
  162. warnings.warn(
  163. "ForwardRequestException(path_info=...) has been deprecated; please "
  164. "use ForwardRequestException(url=...)",
  165. DeprecationWarning, 2)
  166. else:
  167. raise TypeError('You cannot use url and path_info in ForwardRequestException')
  168. self.path_info = path_info
  169. # If the url can be treated as a path_info do that
  170. if url and not '?' in str(url):
  171. self.path_info = url
  172. # Base middleware
  173. class ForwardRequestExceptionMiddleware(object):
  174. def __init__(self, app):
  175. self.app = app
  176. # Otherwise construct the appropriate middleware factory
  177. if hasattr(self, 'path_info'):
  178. p = self.path_info
  179. def factory_(app):
  180. class PathInfoForward(ForwardRequestExceptionMiddleware):
  181. def __call__(self, environ, start_response):
  182. environ['PATH_INFO'] = p
  183. return self.app(environ, start_response)
  184. return PathInfoForward(app)
  185. self.factory = factory_
  186. elif url:
  187. def factory_(app):
  188. class URLForward(ForwardRequestExceptionMiddleware):
  189. def __call__(self, environ, start_response):
  190. environ['PATH_INFO'] = url.split('?')[0]
  191. environ['QUERY_STRING'] = url.split('?')[1]
  192. return self.app(environ, start_response)
  193. return URLForward(app)
  194. self.factory = factory_
  195. elif environ:
  196. def factory_(app):
  197. class EnvironForward(ForwardRequestExceptionMiddleware):
  198. def __call__(self, environ_, start_response):
  199. return self.app(environ, start_response)
  200. return EnvironForward(app)
  201. self.factory = factory_
  202. else:
  203. self.factory = factory
  204. class Recursive(object):
  205. def __init__(self, application, environ, start_response):
  206. self.application = application
  207. self.original_environ = environ.copy()
  208. self.previous_environ = environ
  209. self.start_response = start_response
  210. def __call__(self, path, extra_environ=None):
  211. """
  212. `extra_environ` is an optional dictionary that is also added
  213. to the forwarded request. E.g., ``{'HTTP_HOST': 'new.host'}``
  214. could be used to forward to a different virtual host.
  215. """
  216. environ = self.original_environ.copy()
  217. if extra_environ:
  218. environ.update(extra_environ)
  219. environ['paste.recursive.previous_environ'] = self.previous_environ
  220. base_path = self.original_environ.get('SCRIPT_NAME')
  221. if path.startswith('/'):
  222. assert path.startswith(base_path), (
  223. "You can only forward requests to resources under the "
  224. "path %r (not %r)" % (base_path, path))
  225. path = path[len(base_path)+1:]
  226. assert not path.startswith('/')
  227. path_info = '/' + path
  228. environ['PATH_INFO'] = path_info
  229. environ['REQUEST_METHOD'] = 'GET'
  230. environ['CONTENT_LENGTH'] = '0'
  231. environ['CONTENT_TYPE'] = ''
  232. environ['wsgi.input'] = StringIO('')
  233. return self.activate(environ)
  234. def activate(self, environ):
  235. raise NotImplementedError
  236. def __repr__(self):
  237. return '<%s.%s from %s>' % (
  238. self.__class__.__module__,
  239. self.__class__.__name__,
  240. self.original_environ.get('SCRIPT_NAME') or '/')
  241. class Forwarder(Recursive):
  242. """
  243. The forwarder will try to restart the request, except with
  244. the new `path` (replacing ``PATH_INFO`` in the request).
  245. It must not be called after and headers have been returned.
  246. It returns an iterator that must be returned back up the call
  247. stack, so it must be used like:
  248. .. code-block:: python
  249. return environ['paste.recursive.forward'](path)
  250. Meaningful transformations cannot be done, since headers are
  251. sent directly to the server and cannot be inspected or
  252. rewritten.
  253. """
  254. def activate(self, environ):
  255. warnings.warn(
  256. "recursive.Forwarder has been deprecated; please use "
  257. "ForwardRequestException",
  258. DeprecationWarning, 2)
  259. return self.application(environ, self.start_response)
  260. class Includer(Recursive):
  261. """
  262. Starts another request with the given path and adding or
  263. overwriting any values in the `extra_environ` dictionary.
  264. Returns an IncludeResponse object.
  265. """
  266. def activate(self, environ):
  267. response = IncludedResponse()
  268. def start_response(status, headers, exc_info=None):
  269. if exc_info:
  270. six.reraise(exc_info[0], exc_info[1], exc_info[2])
  271. response.status = status
  272. response.headers = headers
  273. return response.write
  274. app_iter = self.application(environ, start_response)
  275. try:
  276. for s in app_iter:
  277. response.write(s)
  278. finally:
  279. if hasattr(app_iter, 'close'):
  280. app_iter.close()
  281. response.close()
  282. return response
  283. class IncludedResponse(object):
  284. def __init__(self):
  285. self.headers = None
  286. self.status = None
  287. self.output = StringIO()
  288. self.str = None
  289. def close(self):
  290. self.str = self.output.getvalue()
  291. self.output.close()
  292. self.output = None
  293. def write(self, s):
  294. assert self.output is not None, (
  295. "This response has already been closed and no further data "
  296. "can be written.")
  297. self.output.write(s)
  298. def __str__(self):
  299. return self.body
  300. def body__get(self):
  301. if self.str is None:
  302. return self.output.getvalue()
  303. else:
  304. return self.str
  305. body = property(body__get)
  306. class IncluderAppIter(Recursive):
  307. """
  308. Like Includer, but just stores the app_iter response
  309. (be sure to call close on the response!)
  310. """
  311. def activate(self, environ):
  312. response = IncludedAppIterResponse()
  313. def start_response(status, headers, exc_info=None):
  314. if exc_info:
  315. six.reraise(exc_info[0], exc_info[1], exc_info[2])
  316. response.status = status
  317. response.headers = headers
  318. return response.write
  319. app_iter = self.application(environ, start_response)
  320. response.app_iter = app_iter
  321. return response
  322. class IncludedAppIterResponse(object):
  323. def __init__(self):
  324. self.status = None
  325. self.headers = None
  326. self.accumulated = []
  327. self.app_iter = None
  328. self._closed = False
  329. def close(self):
  330. assert not self._closed, (
  331. "Tried to close twice")
  332. if hasattr(self.app_iter, 'close'):
  333. self.app_iter.close()
  334. def write(self, s):
  335. self.accumulated.append
  336. def make_recursive_middleware(app, global_conf):
  337. return RecursiveMiddleware(app)
  338. make_recursive_middleware.__doc__ = __doc__