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

root/branches/cherrypy-3.0.x/cherrypy/_cpwsgi.py

Revision 1711 (checked in by fumanchu, 1 year ago)

Great progress on #718 (High count of uncollectable objects). Folded the separate InternalRedirect? WSGI middleware back into AppResponse?.

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets