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