Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(258)

Side by Side Diff: third_party/cherrypy/_cpwsgi.py

Issue 9368042: Add CherryPy to third_party. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build/
Patch Set: '' Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/cherrypy/_cptree.py ('k') | third_party/cherrypy/_cpwsgi_server.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
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
10 import sys as _sys
11
12 import cherrypy as _cherrypy
13 from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr
14 from cherrypy import _cperror
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
31
32
33 class VirtualHost(object):
34 """Select a different WSGI application based on the Host header.
35
36 This can be useful when running multiple sites within one CP server.
37 It allows several domains to point to different applications. For example::
38
39 root = Root()
40 RootApp = cherrypy.Application(root)
41 Domain2App = cherrypy.Application(root)
42 SecureApp = cherrypy.Application(Secure())
43
44 vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
45 domains={'www.domain2.example': Domain2App,
46 'www.domain2.example:443': SecureApp,
47 })
48
49 cherrypy.tree.graft(vhost)
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.
66 """
67
68 def __init__(self, default, domains=None, use_x_forwarded_host=True):
69 self.default = default
70 self.domains = domains or {}
71 self.use_x_forwarded_host = use_x_forwarded_host
72
73 def __call__(self, environ, start_response):
74 domain = environ.get('HTTP_HOST', '')
75 if self.use_x_forwarded_host:
76 domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
77
78 nextapp = self.domains.get(domain)
79 if nextapp is None:
80 nextapp = self.default
81 return nextapp(environ, start_response)
82
83
84 class InternalRedirector(object):
85 """WSGI middleware that handles raised cherrypy.InternalRedirect."""
86
87 def __init__(self, nextapp, recursive=False):
88 self.nextapp = nextapp
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.thro ws)
137
138
139 class _TrappedResponse(object):
140
141 response = iter([])
142
143 def __init__(self, nextapp, environ, start_response, throws):
144 self.nextapp = nextapp
145 self.environ = environ
146 self.start_response = start_response
147 self.throws = throws
148 self.started_response = False
149 self.response = self.trap(self.nextapp, self.environ, self.start_respons e)
150 self.iter_response = iter(self.response)
151
152 def __iter__(self):
153 self.started_response = True
154 return self
155
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):
168 try:
169 return func(*args, **kwargs)
170 except self.throws:
171 raise
172 except StopIteration:
173 raise
174 except:
175 tb = _cperror.format_exc()
176 #print('trapped (started %s):' % self.started_response, tb)
177 _cherrypy.log(tb, severity=40)
178 if not _cherrypy.request.show_tracebacks:
179 tb = ""
180 s, h, b = _cperror.bare_error(tb)
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
192 try:
193 self.start_response(s, h, _sys.exc_info())
194 except:
195 # "The application must not trap any exceptions raised by
196 # start_response, if it called start_response with exc_info.
197 # Instead, it should allow such exceptions to propagate
198 # back to the server or gateway."
199 # But we still log and call close() to clean up ourselves.
200 _cherrypy.log(traceback=True, severity=40)
201 raise
202
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 s tring." % 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 statu s
240 # and headers must be bytes masquerading as unicode; that is, th ey
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()
262
263 def close(self):
264 """Close and de-reference the current request and response. (Core)"""
265 self.cpapp.release_serving()
266
267 def run(self):
268 """Create a Request object using environ."""
269 env = self.environ.get
270
271 local = httputil.Host('', int(env('SERVER_PORT', 80)),
272 env('SERVER_NAME', ''))
273 remote = httputil.Host(env('REMOTE_ADDR', ''),
274 int(env('REMOTE_PORT', -1) or -1),
275 env('REMOTE_HOST', ''))
276 scheme = env('wsgi.url_scheme')
277 sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
278 request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
279
280 # LOGON_USER is served by IIS, and is the name of the
281 # user after having been mapped to a local account.
282 # Both IIS and Apache set REMOTE_USER, when possible.
283 request.login = env('LOGON_USER') or env('REMOTE_USER') or None
284 request.multithread = self.environ['wsgi.multithread']
285 request.multiprocess = self.environ['wsgi.multiprocess']
286 request.wsgi_environ = self.environ
287 request.prev = env('cherrypy.previous_request', None)
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 encodin g,
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)
321
322 headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
323 'CONTENT_LENGTH': 'Content-Length',
324 'CONTENT_TYPE': 'Content-Type',
325 'REMOTE_HOST': 'Remote-Host',
326 'REMOTE_ADDR': 'Remote-Addr',
327 }
328
329 def translate_headers(self, environ):
330 """Translate CGI-environ header names to HTTP header names."""
331 for cgiName in environ:
332 # We assume all incoming header keys are uppercase already.
333 if cgiName in self.headerNames:
334 yield self.headerNames[cgiName], environ[cgiName]
335 elif cgiName[:5] == "HTTP_":
336 # Hackish attempt at recovering original header names.
337 translatedHeader = cgiName[5:].replace("_", "-")
338 yield translatedHeader, environ[cgiName]
339
340
341 class CPWSGIApp(object):
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
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
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."""
362
363 response_class = AppResponse
364 """The class to instantiate and return as the next app in the WSGI chain."""
365
366 def __init__(self, cpapp, pipeline=None):
367 self.cpapp = cpapp
368 self.pipeline = self.pipeline[:]
369 if pipeline:
370 self.pipeline.extend(pipeline)
371 self.config = self.config.copy()
372
373 def tail(self, environ, start_response):
374 """WSGI application callable for the actual CherryPy application.
375
376 You probably shouldn't call this; call self.__call__ instead,
377 so that any WSGI middleware in self.pipeline can run first.
378 """
379 return self.response_class(environ, start_response, self.cpapp)
380
381 def __call__(self, environ, start_response):
382 head = self.head
383 if head is None:
384 # Create and nest the WSGI apps in our pipeline (in reverse order).
385 # Then memoize the result in self.head.
386 head = self.tail
387 for name, callable in self.pipeline[::-1]:
388 conf = self.config.get(name, {})
389 head = callable(head, **conf)
390 self.head = head
391 return head(environ, start_response)
392
393 def namespace_handler(self, k, v):
394 """Config handler for the 'wsgi' namespace."""
395 if k == "pipeline":
396 # Note this allows multiple 'wsgi.pipeline' config entries
397 # (but each entry will be processed in a 'random' order).
398 # It should also allow developers to set default middleware
399 # in code (passed to self.__init__) that deployers can add to
400 # (but not remove) via config.
401 self.pipeline.extend(v)
402 elif k == "response_class":
403 self.response_class = v
404 else:
405 name, arg = k.split(".", 1)
406 bucket = self.config.setdefault(name, {})
407 bucket[arg] = v
408
OLDNEW
« no previous file with comments | « third_party/cherrypy/_cptree.py ('k') | third_party/cherrypy/_cpwsgi_server.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698