Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/_cpwsgi.py

Revision 1938 (checked in by fumanchu, 1 month ago)

Audit of logging:

  1. cherrypy.log now defaults to INFO.
  2. Bus.log now takes a 'level=INFO' arg.
  3. 'log' channel subscribers now must accept a 'level' arg.
  4. Some bus plugin messages upgraded to WARN or ERROR.
  5. Some CP core messages upgraded to ERROR.
  6. log.screen move to stderr polished. See #747.
  • Property svn:eol-style set to native
Line 
1 """WSGI interface (see PEP 333)."""
2
3 import StringIO as _StringIO
4 import sys as _sys
5
6 import cherrypy as _cherrypy
7 from cherrypy import _cperror, wsgiserver
8 from cherrypy.lib import http as _http
9
10
11 class VirtualHost(object):
12     """Select a different WSGI application based on the Host header.
13     
14     This can be useful when running multiple sites within one CP server.
15     It allows several domains to point to different applications. For example:
16     
17         root = Root()
18         RootApp = cherrypy.Application(root)
19         Domain2App = cherrypy.Application(root)
20         SecureApp = cherrypy.Application(Secure())
21         
22         vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
23             domains={'www.domain2.example': Domain2App,
24                      'www.domain2.example:443': SecureApp,
25                      })
26         
27         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.
41     """
42    
43     def __init__(self, default, domains=None, use_x_forwarded_host=True):
44         self.default = default
45         self.domains = domains or {}
46         self.use_x_forwarded_host = use_x_forwarded_host
47    
48     def __call__(self, environ, start_response):
49         domain = environ.get('HTTP_HOST', '')
50         if self.use_x_forwarded_host:
51             domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
52        
53         nextapp = self.domains.get(domain)
54         if nextapp is None:
55             nextapp = self.default
56         return nextapp(environ, start_response)
57
58
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 = []
70         self.recursive = recursive
71         self.environ = environ
72         self.start_response = start_response
73         self.cpapp = cpapp
74         self.setapp()
75    
76     def setapp(self):
77         # Stage 1: by whatever means necessary, obtain a status, header
78         #          set and body, with an optional exception object.
79         try:
80             self.request = self.get_request()
81             s, h, b = self.get_response()
82             exc = None
83         except self.throws:
84             self.close()
85             raise
86         except _cherrypy.InternalRedirect, ir:
87             self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
88             self.close()
89             self.iredirect(ir.path, ir.query_string)
90             return
91         except:
92             if getattr(self.request, "throw_errors", False):
93                 self.close()
94                 raise
95            
96             tb = _cperror.format_exc()
97             _cherrypy.log(tb, severity=40)
98             if not getattr(self.request, "show_tracebacks", True):
99                 tb = ""
100             s, h, b = _cperror.bare_error(tb)
101             exc = _sys.exc_info()
102        
103         self.iter_response = iter(b)
104        
105         # Stage 2: now that we have a status, header set, and body,
106         #          finish the WSGI conversation by calling start_response.
107         try:
108             self.start_response(s, h, exc)
109         except self.throws:
110             self.close()
111             raise
112         except:
113             if getattr(self.request, "throw_errors", False):
114                 self.close()
115                 raise
116            
117             _cherrypy.log(traceback=True, severity=40)
118             self.close()
119            
120             # CherryPy test suite expects bare_error body to be output,
121             # so don't call start_response (which, according to PEP 333,
122             # may raise its own error at that point).
123             s, h, b = _cperror.bare_error()
124             self.iter_response = iter(b)
125    
126     def iredirect(self, path, query_string):
127         """Doctor self.environ and perform an internal redirect.
128         
129         When cherrypy.InternalRedirect is raised, this method is called.
130         It rewrites the WSGI environ using the new path and query_string,
131         and calls a new CherryPy Request object. Because the wsgi.input
132         stream may have already been consumed by the next application,
133         the redirected call will always be of HTTP method "GET"; therefore,
134         any params must be passed in the query_string argument, which is
135         formed from InternalRedirect.query_string when using that exception.
136         If you need something more complicated, make and raise your own
137         exception and write your own AppResponse subclass to trap it. ;)
138         
139         It would be a bad idea to redirect after you've already yielded
140         response content, although an enterprising soul could choose
141         to abuse this.
142         """
143         env = self.environ
144         if not self.recursive:
145             sn = env.get('SCRIPT_NAME', '')
146             qs = query_string
147             if qs:
148                 qs = "?" + qs
149             if sn + path + qs in self.redirections:
150                 raise RuntimeError("InternalRedirector visited the "
151                                    "same URL twice: %r + %r + %r" %
152                                    (sn, path, qs))
153             else:
154                 # Add the *previous* path_info + qs to redirections.
155                 p = env.get('PATH_INFO', '')
156                 qs = env.get('QUERY_STRING', '')
157                 if qs:
158                     qs = "?" + qs
159                 self.redirections.append(sn + p + qs)
160        
161         # Munge environment and try again.
162         env['REQUEST_METHOD'] = "GET"
163         env['PATH_INFO'] = path
164         env['QUERY_STRING'] = query_string
165         env['wsgi.input'] = _StringIO.StringIO()
166         env['CONTENT_LENGTH'] = "0"
167        
168         self.setapp()
169    
170     def __iter__(self):
171         return self
172    
173     def next(self):
174         try:
175             chunk = self.iter_response.next()
176             # WSGI requires all data to be of type "str". This coercion should
177             # not take any time at all if chunk is already of type "str".
178             # If it's unicode, it could be a big performance hit (x ~500).
179             if not isinstance(chunk, str):
180                 chunk = chunk.encode("ISO-8859-1")
181             return chunk
182         except self.throws:
183             self.close()
184             raise
185         except _cherrypy.InternalRedirect, ir:
186             self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
187             self.close()
188             self.iredirect(ir.path, ir.query_string)
189         except StopIteration:
190             raise
191         except:
192             if getattr(self.request, "throw_errors", False):
193                 self.close()
194                 raise
195            
196             _cherrypy.log(traceback=True, severity=40)
197            
198             # CherryPy test suite expects bare_error body to be output,
199             # so don't call start_response (which, according to PEP 333,
200             # may raise its own error at that point).
201             s, h, b = _cperror.bare_error()
202             self.iter_response = iter([])
203             return "".join(b)
204    
205     def close(self):
206         """Close and de-reference the current request and response. (Core)"""
207         self.cpapp.release_serving()
208    
209     def get_response(self):
210         """Run self.request and return its response."""
211         meth = self.environ['REQUEST_METHOD']
212         path = _http.urljoin(self.environ.get('SCRIPT_NAME', ''),
213                              self.environ.get('PATH_INFO', ''))
214         qs = self.environ.get('QUERY_STRING', '')
215         rproto = self.environ.get('SERVER_PROTOCOL')
216         headers = self.translate_headers(self.environ)
217         rfile = self.environ['wsgi.input']
218         response = self.request.run(meth, path, qs, rproto, headers, rfile)
219         return response.status, response.header_list, response.body
220    
221     def get_request(self):
222         """Create a Request object using environ."""
223         env = self.environ.get
224        
225         local = _http.Host('', int(env('SERVER_PORT', 80)),
226                            env('SERVER_NAME', ''))
227         remote = _http.Host(env('REMOTE_ADDR', ''),
228                             int(env('REMOTE_PORT', -1)),
229                             env('REMOTE_HOST', ''))
230         scheme = env('wsgi.url_scheme')
231         sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
232         request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
233        
234         # LOGON_USER is served by IIS, and is the name of the
235         # user after having been mapped to a local account.
236         # Both IIS and Apache set REMOTE_USER, when possible.
237         request.login = env('LOGON_USER') or env('REMOTE_USER') or None
238         request.multithread = self.environ['wsgi.multithread']
239         request.multiprocess = self.environ['wsgi.multiprocess']
240         request.wsgi_environ = self.environ
241         request.prev = env('cherrypy.previous_request', None)
242         return request
243    
244     headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
245                    'CONTENT_LENGTH': 'Content-Length',
246                    'CONTENT_TYPE': 'Content-Type',
247                    'REMOTE_HOST': 'Remote-Host',
248                    'REMOTE_ADDR': 'Remote-Addr',
249                    }
250    
251     def translate_headers(self, environ):
252         """Translate CGI-environ header names to HTTP header names."""
253         for cgiName in environ:
254             # We assume all incoming header keys are uppercase already.
255             if cgiName in self.headerNames:
256                 yield self.headerNames[cgiName], environ[cgiName]
257             elif cgiName[:5] == "HTTP_":
258                 # Hackish attempt at recovering original header names.
259                 translatedHeader = cgiName[5:].replace("_", "-")
260                 yield translatedHeader, environ[cgiName]
261
262
263 class CPWSGIApp(object):
264     """A WSGI application object for a CherryPy Application.
265     
266     pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
267         constructor that takes an initial, positional 'nextapp' argument,
268         plus optional keyword arguments, and returns a WSGI application
269         (that takes environ and start_response arguments). The 'name' can
270         be any you choose, and will correspond to keys in self.config.
271     
272     head: rather than nest all apps in the pipeline on each call, it's only
273         done the first time, and the result is memoized into self.head. Set
274         this to None again if you change self.pipeline after calling self.
275     
276     config: a dict whose keys match names listed in the pipeline. Each
277         value is a further dict which will be passed to the corresponding
278         named WSGI callable (from the pipeline) as keyword arguments.
279     """
280    
281     pipeline = []
282     head = None
283     config = {}
284    
285     response_class = AppResponse
286    
287     def __init__(self, cpapp, pipeline=None):
288         self.cpapp = cpapp
289         self.pipeline = self.pipeline[:]
290         if pipeline:
291             self.pipeline.extend(pipeline)
292         self.config = self.config.copy()
293    
294     def tail(self, environ, start_response):
295         """WSGI application callable for the actual CherryPy application.
296         
297         You probably shouldn't call this; call self.__call__ instead,
298         so that any WSGI middleware in self.pipeline can run first.
299         """
300         return self.response_class(environ, start_response, self.cpapp)
301    
302     def __call__(self, environ, start_response):
303         head = self.head
304         if head is None:
305             # Create and nest the WSGI apps in our pipeline (in reverse order).
306             # Then memoize the result in self.head.
307             head = self.tail
308             for name, callable in self.pipeline[::-1]:
309                 conf = self.config.get(name, {})
310                 head = callable(head, **conf)
311             self.head = head
312         return head(environ, start_response)
313    
314     def namespace_handler(self, k, v):
315         """Config handler for the 'wsgi' namespace."""
316         if k == "pipeline":
317             # Note this allows multiple 'wsgi.pipeline' config entries
318             # (but each entry will be processed in a 'random' order).
319             # It should also allow developers to set default middleware
320             # in code (passed to self.__init__) that deployers can add to
321             # (but not remove) via config.
322             self.pipeline.extend(v)
323         else:
324             name, arg = k.split(".", 1)
325             bucket = self.config.setdefault(name, {})
326             bucket[arg] = v
327
328
329
330 #                            Server components                            #
331
332
333 class CPHTTPRequest(wsgiserver.HTTPRequest):
334    
335     def __init__(self, sendall, environ, wsgi_app):
336         s = _cherrypy.server
337         self.max_request_header_size = s.max_request_header_size or 0
338         self.max_request_body_size = s.max_request_body_size or 0
339         wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app)
340
341
342 class CPHTTPConnection(wsgiserver.HTTPConnection):
343    
344     RequestHandlerClass = CPHTTPRequest
345
346
347 class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
348     """Wrapper for wsgiserver.CherryPyWSGIServer.
349     
350     wsgiserver has been designed to not reference CherryPy in any way,
351     so that it can be used in other frameworks and applications. Therefore,
352     we wrap it here, so we can set our own mount points from cherrypy.tree
353     and apply some attributes from config -> cherrypy.server -> wsgiserver.
354     """
355    
356     ConnectionClass = CPHTTPConnection
357    
358     def __init__(self):
359         server = _cherrypy.server
360         sockFile = server.socket_file
361         if sockFile:
362             bind_addr = sockFile
363         else:
364             bind_addr = (server.socket_host, server.socket_port)
365        
366         s = wsgiserver.CherryPyWSGIServer
367         s.__init__(self, bind_addr, _cherrypy.tree,
368                    server.thread_pool,
369                    server.socket_host,
370                    request_queue_size = server.socket_queue_size,
371                    timeout = server.socket_timeout,
372                    shutdown_timeout = server.shutdown_timeout,
373                    )
374         self.protocol = server.protocol_version
375         self.nodelay = server.nodelay
376         self.ssl_certificate = server.ssl_certificate
377         self.ssl_private_key = server.ssl_private_key
378
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets