| 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__ | 
|---|