1
"""WSGI interface (see PEP 333)."""
3
import StringIO as _StringIO
1
"""WSGI interface (see PEP 333 and 3333).
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!"
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
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."""
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
11
33
class VirtualHost(object):
12
34
"""Select a different WSGI application based on the Host header.
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::
18
40
RootApp = cherrypy.Application(root)
27
49
cherrypy.tree.graft(vhost)
29
default: required. The default WSGI application.
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.
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.
52
"""Required. The default WSGI application."""
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."""
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.
43
68
def __init__(self, default, domains=None, use_x_forwarded_host=True):
56
81
return nextapp(environ, start_response)
60
# WSGI-to-CP Adapter #
63
class AppResponse(object):
65
throws = (KeyboardInterrupt, SystemExit)
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."""
87
def __init__(self, nextapp, recursive=False):
88
self.nextapp = nextapp
70
89
self.recursive = recursive
91
def __call__(self, environ, start_response):
94
environ = environ.copy()
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', '')
103
# Add the *previous* path_info + qs to redirections.
107
redirections.append(old_uri)
109
if not self.recursive:
110
# Check to see if the new URI has been redirected to already
111
new_uri = sn + ir.path
113
new_uri += "?" + ir.query_string
114
if new_uri in redirections:
116
raise RuntimeError("InternalRedirector visited the "
117
"same URL twice: %r" % new_uri)
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
128
class ExceptionTrapper(object):
129
"""WSGI middleware that traps exceptions."""
131
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
132
self.nextapp = nextapp
135
def __call__(self, environ, start_response):
136
return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
139
class _TrappedResponse(object):
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
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)
85
except _cherrypy.InternalRedirect, ir:
86
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
88
self.iredirect(ir.path, ir.query_string)
91
if getattr(self.request, "throw_errors", False):
95
tb = _cperror.format_exc()
96
_cherrypy.log(tb, severity=40)
97
if not getattr(self.request, "show_tracebacks", True):
99
s, h, b = _cperror.bare_error(tb)
100
self.iter_response = iter(b)
103
self.start_response(s, h, _sys.exc_info())
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)
114
def iredirect(self, path, query_string):
115
"""Doctor self.environ and perform an internal redirect.
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. ;)
127
It would be a bad idea to redirect after you've already yielded
128
response content, although an enterprising soul could choose
132
if not self.recursive:
133
sn = env.get('SCRIPT_NAME', '')
137
if sn + path + qs in self.redirections:
138
raise RuntimeError("InternalRedirector visited the "
139
"same URL twice: %r + %r + %r" %
142
# Add the *previous* path_info + qs to redirections.
143
p = env.get('PATH_INFO', '')
144
qs = env.get('QUERY_STRING', '')
147
self.redirections.append(sn + p + qs)
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"
148
self.started_response = False
149
self.response = self.trap(self.nextapp, self.environ, self.start_response)
150
self.iter_response = iter(self.response)
158
152
def __iter__(self):
153
self.started_response = True
158
return self.trap(next, self.iter_response)
161
return self.trap(self.iter_response.next)
164
if hasattr(self.response, 'close'):
165
self.response.close()
167
def trap(self, func, *args, **kwargs):
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 func(*args, **kwargs)
170
170
except self.throws:
173
except _cherrypy.InternalRedirect, ir:
174
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
176
self.iredirect(ir.path, ir.query_string)
177
172
except StopIteration:
180
if getattr(self.request, "throw_errors", False):
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:
188
180
s, h, b = _cperror.bare_error(tb)
189
# Empty our iterable (so future calls raise StopIteration)
190
self.iter_response = iter([])
183
s = s.decode('ISO-8859-1')
184
h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
186
if self.started_response:
187
# Empty our iterable (so future calls raise StopIteration)
188
self.iter_response = iter([])
190
self.iter_response = iter(b)
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)
203
if self.started_response:
204
return ntob("").join(b)
209
# WSGI-to-CP Adapter #
212
class AppResponse(object):
213
"""WSGI response iterable for CherryPy applications."""
215
def __init__(self, environ, start_response, cpapp):
219
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
220
environ = downgrade_wsgi_ux_to_1x(environ)
221
self.environ = environ
224
r = _cherrypy.serving.response
226
outstatus = r.output_status
227
if not isinstance(outstatus, bytestr):
228
raise TypeError("response.output_status is not a byte string.")
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))
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
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]
247
self.iter_response = iter(r.body)
248
self.write = start_response(outstatus, outheaders)
258
return next(self.iter_response)
261
return self.iter_response.next()
207
264
"""Close and de-reference the current request and response. (Core)"""
208
265
self.cpapp.release_serving()
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
222
def get_request(self):
223
268
"""Create a Request object using environ."""
224
269
env = self.environ.get
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)
289
meth = self.environ['REQUEST_METHOD']
291
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
292
self.environ.get('PATH_INFO', ''))
293
qs = self.environ.get('QUERY_STRING', '')
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.
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.
313
# Only set transcoded values if they both succeed.
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)
245
322
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
246
323
'CONTENT_LENGTH': 'Content-Length',
264
341
class CPWSGIApp(object):
265
"""A WSGI application object for a CherryPy Application.
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.
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.
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.
342
"""A WSGI application object for a CherryPy Application."""
344
pipeline = [('ExceptionTrapper', ExceptionTrapper),
345
('InternalRedirector', InternalRedirector),
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."""
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."""
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."""
286
363
response_class = AppResponse
364
"""The class to instantiate and return as the next app in the WSGI chain."""
288
366
def __init__(self, cpapp, pipeline=None):
289
367
self.cpapp = cpapp