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