Home | History | Annotate | Download | only in paste
      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 import six
     27 import warnings
     28 from six.moves import cStringIO as StringIO
     29 
     30 __all__ = ['RecursiveMiddleware']
     31 __pudge_all__ =  ['RecursiveMiddleware', 'ForwardRequestException']
     32 
     33 class RecursionLoop(AssertionError):
     34     # Subclasses AssertionError for legacy reasons
     35     """Raised when a recursion enters into a loop"""
     36 
     37 class 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 
     54 class 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 
     91 class 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 
    242 class 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 
    284 class 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 
    311 class 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 
    337 class 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 
    367 class 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 
    385 class 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 
    403 def make_recursive_middleware(app, global_conf):
    404     return RecursiveMiddleware(app)
    405 
    406 make_recursive_middleware.__doc__ = __doc__
    407