~ubuntu-branches/debian/jessie/cherrypy3/jessie

« back to all changes in this revision

Viewing changes to cherrypy/_cpwsgi.py

  • Committer: Package Import Robot
  • Author(s): Gustavo Noronha Silva, JCF Ploemen, Stéphane Graber, Gustavo Noronha
  • Date: 2012-01-06 10:13:27 UTC
  • mfrom: (1.1.4) (7.1.2 sid)
  • Revision ID: package-import@ubuntu.com-20120106101327-smxnhguqs14ubl7e
Tags: 3.2.2-1
[ JCF Ploemen ]
* New upstream release (Closes: #571196).
* Bumped Standards-Version to 3.8.4 (no changes needed).
* Removing patch 02: no longer needed, incorporated upstream.
* Updating patch 00 to match release.
* Install cherryd man page via debian/manpages.
* debian/copyright:
  + Added notice for cherrypy/lib/httpauth.py.
  + Fixed years.
* debian/watch:
  + Don't hit on the -py3 release by blocking '-' from the version.
  + Mangle upstream version, inserting a tilde for beta/rc.

[ Stéphane Graber <stgraber@ubuntu.com> ]
 * Convert from python-support to dh_python2 (#654375)
  - debian/pyversions: Removed (no longer needed)
  - debian/rules
   + Replace call to dh_pysupport by dh_python2
   + Add --with=python2 to all dh calls
  - debian/control
   + Drop build-depends on python-support
   + Bump build-depends on python-all to >= 2.6.6-3~
   + Replace XS-Python-Version by X-Python-Version
   + Remove XB-Python-Version from binary package

[ Gustavo Noronha ]
* debian/control, debian/rules, debian/manpages:
 - use help2man to generate a manpage for cherryd at build time, since
  one is no longer shipped along with the source code
* debian/control:
- add python-nose to Build-Depends, since it's used during the
  documentation build for cross-reference generation

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
"""WSGI interface (see PEP 333)."""
2
 
 
3
 
import StringIO as _StringIO
 
1
"""WSGI interface (see PEP 333 and 3333).
 
2
 
 
3
Note that WSGI environ keys and values are 'native strings'; that is,
 
4
whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
 
5
it's a unicode string. But PEP 3333 says: "even if Python's str type is
 
6
actually Unicode "under the hood", the content of native strings must
 
7
still be translatable to bytes via the Latin-1 encoding!"
 
8
"""
 
9
 
4
10
import sys as _sys
5
11
 
6
12
import cherrypy as _cherrypy
 
13
from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr
7
14
from cherrypy import _cperror
8
 
from cherrypy.lib import http as _http
 
15
from cherrypy.lib import httputil
 
16
 
 
17
 
 
18
def downgrade_wsgi_ux_to_1x(environ):
 
19
    """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
 
20
    env1x = {}
 
21
    
 
22
    url_encoding = environ[ntou('wsgi.url_encoding')]
 
23
    for k, v in list(environ.items()):
 
24
        if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
 
25
            v = v.encode(url_encoding)
 
26
        elif isinstance(v, unicodestr):
 
27
            v = v.encode('ISO-8859-1')
 
28
        env1x[k.encode('ISO-8859-1')] = v
 
29
    
 
30
    return env1x
9
31
 
10
32
 
11
33
class VirtualHost(object):
12
34
    """Select a different WSGI application based on the Host header.
13
35
    
14
36
    This can be useful when running multiple sites within one CP server.
15
 
    It allows several domains to point to different applications. For example:
 
37
    It allows several domains to point to different applications. For example::
16
38
    
17
39
        root = Root()
18
40
        RootApp = cherrypy.Application(root)
25
47
                     })
26
48
        
27
49
        cherrypy.tree.graft(vhost)
28
 
    
29
 
    default: required. The default WSGI application.
30
 
    
31
 
    use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
32
 
        request header will be used instead of the "Host" header. This
33
 
        is commonly added by HTTP servers (such as Apache) when proxying.
34
 
    
35
 
    domains: a dict of {host header value: application} pairs.
36
 
        The incoming "Host" request header is looked up in this dict,
37
 
        and, if a match is found, the corresponding WSGI application
38
 
        will be called instead of the default. Note that you often need
39
 
        separate entries for "example.com" and "www.example.com".
40
 
        In addition, "Host" headers may contain the port number.
 
50
    """
 
51
    default = None
 
52
    """Required. The default WSGI application."""
 
53
    
 
54
    use_x_forwarded_host = True
 
55
    """If True (the default), any "X-Forwarded-Host"
 
56
    request header will be used instead of the "Host" header. This
 
57
    is commonly added by HTTP servers (such as Apache) when proxying."""
 
58
    
 
59
    domains = {}
 
60
    """A dict of {host header value: application} pairs.
 
61
    The incoming "Host" request header is looked up in this dict,
 
62
    and, if a match is found, the corresponding WSGI application
 
63
    will be called instead of the default. Note that you often need
 
64
    separate entries for "example.com" and "www.example.com".
 
65
    In addition, "Host" headers may contain the port number.
41
66
    """
42
67
    
43
68
    def __init__(self, default, domains=None, use_x_forwarded_host=True):
56
81
        return nextapp(environ, start_response)
57
82
 
58
83
 
59
 
 
60
 
#                           WSGI-to-CP Adapter                           #
61
 
 
62
 
 
63
 
class AppResponse(object):
64
 
    
65
 
    throws = (KeyboardInterrupt, SystemExit)
66
 
    request = None
67
 
    
68
 
    def __init__(self, environ, start_response, cpapp, recursive=False):
69
 
        self.redirections = []
 
84
class InternalRedirector(object):
 
85
    """WSGI middleware that handles raised cherrypy.InternalRedirect."""
 
86
    
 
87
    def __init__(self, nextapp, recursive=False):
 
88
        self.nextapp = nextapp
70
89
        self.recursive = recursive
 
90
    
 
91
    def __call__(self, environ, start_response):
 
92
        redirections = []
 
93
        while True:
 
94
            environ = environ.copy()
 
95
            try:
 
96
                return self.nextapp(environ, start_response)
 
97
            except _cherrypy.InternalRedirect:
 
98
                ir = _sys.exc_info()[1]
 
99
                sn = environ.get('SCRIPT_NAME', '')
 
100
                path = environ.get('PATH_INFO', '')
 
101
                qs = environ.get('QUERY_STRING', '')
 
102
                
 
103
                # Add the *previous* path_info + qs to redirections.
 
104
                old_uri = sn + path
 
105
                if qs:
 
106
                    old_uri += "?" + qs
 
107
                redirections.append(old_uri)
 
108
                
 
109
                if not self.recursive:
 
110
                    # Check to see if the new URI has been redirected to already
 
111
                    new_uri = sn + ir.path
 
112
                    if ir.query_string:
 
113
                        new_uri += "?" + ir.query_string
 
114
                    if new_uri in redirections:
 
115
                        ir.request.close()
 
116
                        raise RuntimeError("InternalRedirector visited the "
 
117
                                           "same URL twice: %r" % new_uri)
 
118
                
 
119
                # Munge the environment and try again.
 
120
                environ['REQUEST_METHOD'] = "GET"
 
121
                environ['PATH_INFO'] = ir.path
 
122
                environ['QUERY_STRING'] = ir.query_string
 
123
                environ['wsgi.input'] = BytesIO()
 
124
                environ['CONTENT_LENGTH'] = "0"
 
125
                environ['cherrypy.previous_request'] = ir.request
 
126
 
 
127
 
 
128
class ExceptionTrapper(object):
 
129
    """WSGI middleware that traps exceptions."""
 
130
    
 
131
    def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
 
132
        self.nextapp = nextapp
 
133
        self.throws = throws
 
134
    
 
135
    def __call__(self, environ, start_response):
 
136
        return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
 
137
 
 
138
 
 
139
class _TrappedResponse(object):
 
140
    
 
141
    response = iter([])
 
142
    
 
143
    def __init__(self, nextapp, environ, start_response, throws):
 
144
        self.nextapp = nextapp
71
145
        self.environ = environ
72
146
        self.start_response = start_response
73
 
        self.cpapp = cpapp
74
 
        self.setapp()
75
 
    
76
 
    def setapp(self):
77
 
        try:
78
 
            self.request = self.get_request()
79
 
            s, h, b = self.get_response()
80
 
            self.iter_response = iter(b)
81
 
            self.start_response(s, h)
82
 
        except self.throws:
83
 
            self.close()
84
 
            raise
85
 
        except _cherrypy.InternalRedirect, ir:
86
 
            self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
87
 
            self.close()
88
 
            self.iredirect(ir.path, ir.query_string)
89
 
            return
90
 
        except:
91
 
            if getattr(self.request, "throw_errors", False):
92
 
                self.close()
93
 
                raise
94
 
            
95
 
            tb = _cperror.format_exc()
96
 
            _cherrypy.log(tb, severity=40)
97
 
            if not getattr(self.request, "show_tracebacks", True):
98
 
                tb = ""
99
 
            s, h, b = _cperror.bare_error(tb)
100
 
            self.iter_response = iter(b)
101
 
            
102
 
            try:
103
 
                self.start_response(s, h, _sys.exc_info())
104
 
            except:
105
 
                # "The application must not trap any exceptions raised by
106
 
                # start_response, if it called start_response with exc_info.
107
 
                # Instead, it should allow such exceptions to propagate
108
 
                # back to the server or gateway."
109
 
                # But we still log and call close() to clean up ourselves.
110
 
                _cherrypy.log(traceback=True, severity=40)
111
 
                self.close()
112
 
                raise
113
 
    
114
 
    def iredirect(self, path, query_string):
115
 
        """Doctor self.environ and perform an internal redirect.
116
 
        
117
 
        When cherrypy.InternalRedirect is raised, this method is called.
118
 
        It rewrites the WSGI environ using the new path and query_string,
119
 
        and calls a new CherryPy Request object. Because the wsgi.input
120
 
        stream may have already been consumed by the next application,
121
 
        the redirected call will always be of HTTP method "GET"; therefore,
122
 
        any params must be passed in the query_string argument, which is
123
 
        formed from InternalRedirect.query_string when using that exception.
124
 
        If you need something more complicated, make and raise your own
125
 
        exception and write your own AppResponse subclass to trap it. ;)
126
 
        
127
 
        It would be a bad idea to redirect after you've already yielded
128
 
        response content, although an enterprising soul could choose
129
 
        to abuse this.
130
 
        """
131
 
        env = self.environ
132
 
        if not self.recursive:
133
 
            sn = env.get('SCRIPT_NAME', '')
134
 
            qs = query_string
135
 
            if qs:
136
 
                qs = "?" + qs
137
 
            if sn + path + qs in self.redirections:
138
 
                raise RuntimeError("InternalRedirector visited the "
139
 
                                   "same URL twice: %r + %r + %r" %
140
 
                                   (sn, path, qs))
141
 
            else:
142
 
                # Add the *previous* path_info + qs to redirections.
143
 
                p = env.get('PATH_INFO', '')
144
 
                qs = env.get('QUERY_STRING', '')
145
 
                if qs:
146
 
                    qs = "?" + qs
147
 
                self.redirections.append(sn + p + qs)
148
 
        
149
 
        # Munge environment and try again.
150
 
        env['REQUEST_METHOD'] = "GET"
151
 
        env['PATH_INFO'] = path
152
 
        env['QUERY_STRING'] = query_string
153
 
        env['wsgi.input'] = _StringIO.StringIO()
154
 
        env['CONTENT_LENGTH'] = "0"
155
 
        
156
 
        self.setapp()
 
147
        self.throws = throws
 
148
        self.started_response = False
 
149
        self.response = self.trap(self.nextapp, self.environ, self.start_response)
 
150
        self.iter_response = iter(self.response)
157
151
    
158
152
    def __iter__(self):
 
153
        self.started_response = True
159
154
        return self
160
155
    
161
 
    def next(self):
 
156
    if py3k:
 
157
        def __next__(self):
 
158
            return self.trap(next, self.iter_response)
 
159
    else:
 
160
        def next(self):
 
161
            return self.trap(self.iter_response.next)
 
162
    
 
163
    def close(self):
 
164
        if hasattr(self.response, 'close'):
 
165
            self.response.close()
 
166
    
 
167
    def trap(self, func, *args, **kwargs):
162
168
        try:
163
 
            chunk = self.iter_response.next()
164
 
            # WSGI requires all data to be of type "str". This coercion should
165
 
            # not take any time at all if chunk is already of type "str".
166
 
            # If it's unicode, it could be a big performance hit (x ~500).
167
 
            if not isinstance(chunk, str):
168
 
                chunk = chunk.encode("ISO-8859-1")
169
 
            return chunk
 
169
            return func(*args, **kwargs)
170
170
        except self.throws:
171
 
            self.close()
172
171
            raise
173
 
        except _cherrypy.InternalRedirect, ir:
174
 
            self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
175
 
            self.close()
176
 
            self.iredirect(ir.path, ir.query_string)
177
172
        except StopIteration:
178
173
            raise
179
174
        except:
180
 
            if getattr(self.request, "throw_errors", False):
181
 
                self.close()
182
 
                raise
183
 
            
184
175
            tb = _cperror.format_exc()
 
176
            #print('trapped (started %s):' % self.started_response, tb)
185
177
            _cherrypy.log(tb, severity=40)
186
 
            if not getattr(self.request, "show_tracebacks", True):
 
178
            if not _cherrypy.request.show_tracebacks:
187
179
                tb = ""
188
180
            s, h, b = _cperror.bare_error(tb)
189
 
            # Empty our iterable (so future calls raise StopIteration)
190
 
            self.iter_response = iter([])
 
181
            if py3k:
 
182
                # What fun.
 
183
                s = s.decode('ISO-8859-1')
 
184
                h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
 
185
                     for k, v in h]
 
186
            if self.started_response:
 
187
                # Empty our iterable (so future calls raise StopIteration)
 
188
                self.iter_response = iter([])
 
189
            else:
 
190
                self.iter_response = iter(b)
191
191
            
192
192
            try:
193
193
                self.start_response(s, h, _sys.exc_info())
198
198
                # back to the server or gateway."
199
199
                # But we still log and call close() to clean up ourselves.
200
200
                _cherrypy.log(traceback=True, severity=40)
201
 
                self.close()
202
201
                raise
203
202
            
204
 
            return "".join(b)
 
203
            if self.started_response:
 
204
                return ntob("").join(b)
 
205
            else:
 
206
                return b
 
207
 
 
208
 
 
209
#                           WSGI-to-CP Adapter                           #
 
210
 
 
211
 
 
212
class AppResponse(object):
 
213
    """WSGI response iterable for CherryPy applications."""
 
214
    
 
215
    def __init__(self, environ, start_response, cpapp):
 
216
        self.cpapp = cpapp
 
217
        try:
 
218
            if not py3k:
 
219
                if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
 
220
                    environ = downgrade_wsgi_ux_to_1x(environ)
 
221
            self.environ = environ
 
222
            self.run()
 
223
 
 
224
            r = _cherrypy.serving.response
 
225
 
 
226
            outstatus = r.output_status
 
227
            if not isinstance(outstatus, bytestr):
 
228
                raise TypeError("response.output_status is not a byte string.")
 
229
            
 
230
            outheaders = []
 
231
            for k, v in r.header_list:
 
232
                if not isinstance(k, bytestr):
 
233
                    raise TypeError("response.header_list key %r is not a byte string." % k)
 
234
                if not isinstance(v, bytestr):
 
235
                    raise TypeError("response.header_list value %r is not a byte string." % v)
 
236
                outheaders.append((k, v))
 
237
            
 
238
            if py3k:
 
239
                # According to PEP 3333, when using Python 3, the response status
 
240
                # and headers must be bytes masquerading as unicode; that is, they
 
241
                # must be of type "str" but are restricted to code points in the
 
242
                # "latin-1" set.
 
243
                outstatus = outstatus.decode('ISO-8859-1')
 
244
                outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
 
245
                              for k, v in outheaders]
 
246
 
 
247
            self.iter_response = iter(r.body)
 
248
            self.write = start_response(outstatus, outheaders)
 
249
        except:
 
250
            self.close()
 
251
            raise
 
252
    
 
253
    def __iter__(self):
 
254
        return self
 
255
    
 
256
    if py3k:
 
257
        def __next__(self):
 
258
            return next(self.iter_response)
 
259
    else:
 
260
        def next(self):
 
261
            return self.iter_response.next()
205
262
    
206
263
    def close(self):
207
264
        """Close and de-reference the current request and response. (Core)"""
208
265
        self.cpapp.release_serving()
209
266
    
210
 
    def get_response(self):
211
 
        """Run self.request and return its response."""
212
 
        meth = self.environ['REQUEST_METHOD']
213
 
        path = _http.urljoin(self.environ.get('SCRIPT_NAME', ''),
214
 
                             self.environ.get('PATH_INFO', ''))
215
 
        qs = self.environ.get('QUERY_STRING', '')
216
 
        rproto = self.environ.get('SERVER_PROTOCOL')
217
 
        headers = self.translate_headers(self.environ)
218
 
        rfile = self.environ['wsgi.input']
219
 
        response = self.request.run(meth, path, qs, rproto, headers, rfile)
220
 
        return response.status, response.header_list, response.body
221
 
    
222
 
    def get_request(self):
 
267
    def run(self):
223
268
        """Create a Request object using environ."""
224
269
        env = self.environ.get
225
270
        
226
 
        local = _http.Host('', int(env('SERVER_PORT', 80)),
 
271
        local = httputil.Host('', int(env('SERVER_PORT', 80)),
227
272
                           env('SERVER_NAME', ''))
228
 
        remote = _http.Host(env('REMOTE_ADDR', ''),
229
 
                            int(env('REMOTE_PORT', -1)),
230
 
                            env('REMOTE_HOST', ''))
 
273
        remote = httputil.Host(env('REMOTE_ADDR', ''),
 
274
                               int(env('REMOTE_PORT', -1) or -1),
 
275
                               env('REMOTE_HOST', ''))
231
276
        scheme = env('wsgi.url_scheme')
232
277
        sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
233
278
        request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
240
285
        request.multiprocess = self.environ['wsgi.multiprocess']
241
286
        request.wsgi_environ = self.environ
242
287
        request.prev = env('cherrypy.previous_request', None)
243
 
        return request
 
288
        
 
289
        meth = self.environ['REQUEST_METHOD']
 
290
        
 
291
        path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
 
292
                                self.environ.get('PATH_INFO', ''))
 
293
        qs = self.environ.get('QUERY_STRING', '')
 
294
 
 
295
        if py3k:
 
296
            # This isn't perfect; if the given PATH_INFO is in the wrong encoding,
 
297
            # it may fail to match the appropriate config section URI. But meh.
 
298
            old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
 
299
            new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''),
 
300
                                             "request.uri_encoding", 'utf-8')
 
301
            if new_enc.lower() != old_enc.lower():
 
302
                # Even though the path and qs are unicode, the WSGI server is
 
303
                # required by PEP 3333 to coerce them to ISO-8859-1 masquerading
 
304
                # as unicode. So we have to encode back to bytes and then decode
 
305
                # again using the "correct" encoding.
 
306
                try:
 
307
                    u_path = path.encode(old_enc).decode(new_enc)
 
308
                    u_qs = qs.encode(old_enc).decode(new_enc)
 
309
                except (UnicodeEncodeError, UnicodeDecodeError):
 
310
                    # Just pass them through without transcoding and hope.
 
311
                    pass
 
312
                else:
 
313
                    # Only set transcoded values if they both succeed.
 
314
                    path = u_path
 
315
                    qs = u_qs
 
316
        
 
317
        rproto = self.environ.get('SERVER_PROTOCOL')
 
318
        headers = self.translate_headers(self.environ)
 
319
        rfile = self.environ['wsgi.input']
 
320
        request.run(meth, path, qs, rproto, headers, rfile)
244
321
    
245
322
    headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
246
323
                   'CONTENT_LENGTH': 'Content-Length',
262
339
 
263
340
 
264
341
class CPWSGIApp(object):
265
 
    """A WSGI application object for a CherryPy Application.
266
 
    
267
 
    pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
268
 
        constructor that takes an initial, positional 'nextapp' argument,
269
 
        plus optional keyword arguments, and returns a WSGI application
270
 
        (that takes environ and start_response arguments). The 'name' can
271
 
        be any you choose, and will correspond to keys in self.config.
272
 
    
273
 
    head: rather than nest all apps in the pipeline on each call, it's only
274
 
        done the first time, and the result is memoized into self.head. Set
275
 
        this to None again if you change self.pipeline after calling self.
276
 
    
277
 
    config: a dict whose keys match names listed in the pipeline. Each
278
 
        value is a further dict which will be passed to the corresponding
279
 
        named WSGI callable (from the pipeline) as keyword arguments.
280
 
    """
281
 
    
282
 
    pipeline = []
 
342
    """A WSGI application object for a CherryPy Application."""
 
343
    
 
344
    pipeline = [('ExceptionTrapper', ExceptionTrapper),
 
345
                ('InternalRedirector', InternalRedirector),
 
346
                ]
 
347
    """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
 
348
    constructor that takes an initial, positional 'nextapp' argument,
 
349
    plus optional keyword arguments, and returns a WSGI application
 
350
    (that takes environ and start_response arguments). The 'name' can
 
351
    be any you choose, and will correspond to keys in self.config."""
 
352
    
283
353
    head = None
 
354
    """Rather than nest all apps in the pipeline on each call, it's only
 
355
    done the first time, and the result is memoized into self.head. Set
 
356
    this to None again if you change self.pipeline after calling self."""
 
357
    
284
358
    config = {}
 
359
    """A dict whose keys match names listed in the pipeline. Each
 
360
    value is a further dict which will be passed to the corresponding
 
361
    named WSGI callable (from the pipeline) as keyword arguments."""
285
362
    
286
363
    response_class = AppResponse
 
364
    """The class to instantiate and return as the next app in the WSGI chain."""
287
365
    
288
366
    def __init__(self, cpapp, pipeline=None):
289
367
        self.cpapp = cpapp