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

root/trunk/cherrypy/_cpwsgi.py

Revision 1994 (checked in by fumanchu, 3 weeks ago)

Fix for #803 (run CP under Google App Engine).

  • 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
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         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()
157    
158     def __iter__(self):
159         return self
160    
161     def next(self):
162         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
170         except self.throws:
171             self.close()
172             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         except StopIteration:
178             raise
179         except:
180             if getattr(self.request, "throw_errors", False):
181                 self.close()
182                 raise
183            
184             tb = _cperror.format_exc()
185             _cherrypy.log(tb, severity=40)
186             if not getattr(self.request, "show_tracebacks", True):
187                 tb = ""
188             s, h, b = _cperror.bare_error(tb)
189             # Empty our iterable (so future calls raise StopIteration)
190             self.iter_response = iter([])
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                 self.close()
202                 raise
203            
204             return "".join(b)
205    
206     def close(self):
207         """Close and de-reference the current request and response. (Core)"""
208         self.cpapp.release_serving()
209    
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):
223         """Create a Request object using environ."""
224         env = self.environ.get
225        
226         local = _http.Host('', int(env('SERVER_PORT', 80)),
227                            env('SERVER_NAME', ''))
228         remote = _http.Host(env('REMOTE_ADDR', ''),
229                             int(env('REMOTE_PORT', -1)),
230                             env('REMOTE_HOST', ''))
231         scheme = env('wsgi.url_scheme')
232         sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
233         request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
234        
235         # LOGON_USER is served by IIS, and is the name of the
236         # user after having been mapped to a local account.
237         # Both IIS and Apache set REMOTE_USER, when possible.
238         request.login = env('LOGON_USER') or env('REMOTE_USER') or None
239         request.multithread = self.environ['wsgi.multithread']
240         request.multiprocess = self.environ['wsgi.multiprocess']
241         request.wsgi_environ = self.environ
242         request.prev = env('cherrypy.previous_request', None)
243         return request
244    
245     headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
246                    'CONTENT_LENGTH': 'Content-Length',
247                    'CONTENT_TYPE': 'Content-Type',
248                    'REMOTE_HOST': 'Remote-Host',
249                    'REMOTE_ADDR': 'Remote-Addr',
250                    }
251    
252     def translate_headers(self, environ):
253         """Translate CGI-environ header names to HTTP header names."""
254         for cgiName in environ:
255             # We assume all incoming header keys are uppercase already.
256             if cgiName in self.headerNames:
257                 yield self.headerNames[cgiName], environ[cgiName]
258             elif cgiName[:5] == "HTTP_":
259                 # Hackish attempt at recovering original header names.
260                 translatedHeader = cgiName[5:].replace("_", "-")
261                 yield translatedHeader, environ[cgiName]
262
263
264 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 = []
283     head = None
284     config = {}
285    
286     response_class = AppResponse
287    
288     def __init__(self, cpapp, pipeline=None):
289         self.cpapp = cpapp
290         self.pipeline = self.pipeline[:]
291         if pipeline:
292             self.pipeline.extend(pipeline)
293         self.config = self.config.copy()
294    
295     def tail(self, environ, start_response):
296         """WSGI application callable for the actual CherryPy application.
297         
298         You probably shouldn't call this; call self.__call__ instead,
299         so that any WSGI middleware in self.pipeline can run first.
300         """
301         return self.response_class(environ, start_response, self.cpapp)
302    
303     def __call__(self, environ, start_response):
304         head = self.head
305         if head is None:
306             # Create and nest the WSGI apps in our pipeline (in reverse order).
307             # Then memoize the result in self.head.
308             head = self.tail
309             for name, callable in self.pipeline[::-1]:
310                 conf = self.config.get(name, {})
311                 head = callable(head, **conf)
312             self.head = head
313         return head(environ, start_response)
314    
315     def namespace_handler(self, k, v):
316         """Config handler for the 'wsgi' namespace."""
317         if k == "pipeline":
318             # Note this allows multiple 'wsgi.pipeline' config entries
319             # (but each entry will be processed in a 'random' order).
320             # It should also allow developers to set default middleware
321             # in code (passed to self.__init__) that deployers can add to
322             # (but not remove) via config.
323             self.pipeline.extend(v)
324         elif k == "response_class":
325             self.response_class = v
326         else:
327             name, arg = k.split(".", 1)
328             bucket = self.config.setdefault(name, {})
329             bucket[arg] = v
330
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets