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