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