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

root/branches/cherrypy-2.x/cherrypy/_cpwsgiserver3.py

Revision 1573 (checked in by fumanchu, 2 years ago)

Copied recent changes from trunk to _cpwsgiserver3.

  • Property svn:eol-style set to native
Line 
1 """A high-speed, production ready, thread pooled, generic WSGI server.
2
3 Simplest example on how to use this module directly
4 (without using CherryPy's application machinery):
5
6     from cherrypy import wsgiserver
7     
8     def my_crazy_app(environ, start_response):
9         status = '200 OK'
10         response_headers = [('Content-type','text/plain')]
11         start_response(status, response_headers)
12         return ['Hello world!\n']
13     
14     # Here we set our application to the script_name '/'
15     wsgi_apps = [('/', my_crazy_app)]
16     
17     server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps,
18                                            server_name='localhost')
19     
20     # Want SSL support? Just set these attributes
21     # server.ssl_certificate = <filename>
22     # server.ssl_private_key = <filename>
23     
24     if __name__ == '__main__':
25         server.start()
26
27 This won't call the CherryPy engine (application side) at all, only the
28 WSGI server, which is independant from the rest of CherryPy. Don't
29 let the name "CherryPyWSGIServer" throw you; the name merely reflects
30 its origin, not it's coupling.
31
32 The CherryPy WSGI server can serve as many WSGI application
33 as you want in one instance:
34
35     wsgi_apps = [('/', my_crazy_app), (/blog', my_blog_app)]
36
37 """
38
39
40 import base64
41 import Queue
42 import os
43 import re
44 quoted_slash = re.compile("(?i)%2F")
45 import rfc822
46 import socket
47 try:
48     import cStringIO as StringIO
49 except ImportError:
50     import StringIO
51 import sys
52 import threading
53 import time
54 import traceback
55 from urllib import unquote
56 from urlparse import urlparse
57
58 try:
59     from OpenSSL import SSL
60     from OpenSSL import crypto
61 except ImportError:
62     SSL = None
63
64 import errno
65 socket_errors_to_ignore = []
66 # Not all of these names will be defined for every platform.
67 for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET",
68           "EHOSTDOWN", "EHOSTUNREACH",
69           "WSAECONNABORTED", "WSAECONNREFUSED", "WSAECONNRESET",
70           "WSAENETRESET", "WSAETIMEDOUT"):
71     if _ in dir(errno):
72         socket_errors_to_ignore.append(getattr(errno, _))
73 # de-dupe the list
74 socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys()
75 socket_errors_to_ignore.append("timed out")
76
77 comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
78     'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
79     'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
80     'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
81     'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
82     'WWW-AUTHENTICATE']
83
84 class HTTPRequest(object):
85     """An HTTP Request (and response).
86     
87     A single HTTP connection may consist of multiple request/response pairs.
88     
89     connection: the HTTP Connection object which spawned this request.
90     rfile: the 'read' fileobject from the connection's socket
91     ready: when True, the request has been parsed and is ready to begin
92         generating the response. When False, signals the calling Connection
93         that the response should not be generated and the connection should
94         close.
95     close_connection: signals the calling Connection that the request
96         should close. This does not imply an error! The client and/or
97         server may each request that the connection be closed.
98     chunked_write: if True, output will be encoded with the "chunked"
99         transfer-coding. This value is set automatically inside
100         send_headers.
101     """
102    
103     def __init__(self, connection):
104         self.connection = connection
105         self.rfile = self.connection.rfile
106         self.sendall = self.connection.sendall
107         self.environ = connection.environ.copy()
108        
109         self.ready = False
110         self.started_response = False
111         self.status = ""
112         self.outheaders = []
113         self.sent_headers = False
114         self.close_connection = False
115         self.chunked_write = False
116    
117     def parse_request(self):
118         """Parse the next HTTP request start-line and message-headers."""
119         # HTTP/1.1 connections are persistent by default. If a client
120         # requests a page, then idles (leaves the connection open),
121         # then rfile.readline() will raise socket.error("timed out").
122         # Note that it does this based on the value given to settimeout(),
123         # and doesn't need the client to request or acknowledge the close
124         # (although your TCP stack might suffer for it: cf Apache's history
125         # with FIN_WAIT_2).
126         request_line = self.rfile.readline()
127         if not request_line:
128             # Force self.ready = False so the connection will close.
129             self.ready = False
130             return
131        
132         if request_line == "\r\n":
133             # RFC 2616 sec 4.1: "...if the server is reading the protocol
134             # stream at the beginning of a message and receives a CRLF
135             # first, it should ignore the CRLF."
136             # But only ignore one leading line! else we enable a DoS.
137             request_line = self.rfile.readline()
138             if not request_line:
139                 self.ready = False
140                 return
141        
142         server = self.connection.server
143         environ = self.environ
144         environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version
145        
146         method, path, req_protocol = request_line.strip().split(" ", 2)
147         environ["REQUEST_METHOD"] = method
148        
149         # path may be an abs_path (including "http://host.domain.tld");
150         scheme, location, path, params, qs, frag = urlparse(path)
151        
152         if frag:
153             self.simple_response("400 Bad Request",
154                                  "Illegal #fragment in Request-URI.")
155             return
156        
157         if scheme:
158             environ["wsgi.url_scheme"] = scheme
159         if params:
160             path = path + ";" + params
161        
162         # Unquote the path+params (e.g. "/this%20path" -> "this path").
163         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
164         #
165         # But note that "...a URI must be separated into its components
166         # before the escaped characters within those components can be
167         # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
168         atoms = [unquote(x) for x in quoted_slash.split(path)]
169         path = "%2F".join(atoms)
170        
171         if path == "*":
172             # This means, of course, that the last wsgi_app (shortest path)
173             # will always handle a URI of "*".
174             environ["SCRIPT_NAME"] = ""
175             environ["PATH_INFO"] = "*"
176             self.wsgi_app = server.mount_points[-1][1]
177         else:
178             for mount_point, wsgi_app in server.mount_points:
179                 # The mount_points list should be sorted by length, descending.
180                 if path.startswith(mount_point + "/") or path == mount_point:
181                     environ["SCRIPT_NAME"] = mount_point
182                     environ["PATH_INFO"] = path[len(mount_point):]
183                     self.wsgi_app = wsgi_app
184                     break
185             else:
186                 self.simple_response("404 Not Found")
187                 return
188        
189         # Note that, like wsgiref and most other WSGI servers,
190         # we unquote the path but not the query string.
191         environ["QUERY_STRING"] = qs
192        
193         # Compare request and server HTTP protocol versions, in case our
194         # server does not support the requested protocol. Limit our output
195         # to min(req, server). We want the following output:
196         #     request    server     actual written   supported response
197         #     protocol   protocol  response protocol    feature set
198         # a     1.0        1.0           1.0                1.0
199         # b     1.0        1.1           1.1                1.0
200         # c     1.1        1.0           1.0                1.0
201         # d     1.1        1.1           1.1                1.1
202         # Notice that, in (b), the response will be "HTTP/1.1" even though
203         # the client only understands 1.0. RFC 2616 10.5.6 says we should
204         # only return 505 if the _major_ version is different.
205         rp = int(req_protocol[5]), int(req_protocol[7])
206         sp = int(server.protocol[5]), int(server.protocol[7])
207         if sp[0] != rp[0]:
208             self.simple_response("505 HTTP Version Not Supported")
209             return
210         # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
211         environ["SERVER_PROTOCOL"] = req_protocol
212         # set a non-standard environ entry so the WSGI app can know what
213         # the *real* server protocol is (and what features to support).
214         # See http://www.faqs.org/rfcs/rfc2145.html.
215         environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol
216         self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
217        
218         # If the Request-URI was an absoluteURI, use its location atom.
219         if location:
220             environ["SERVER_NAME"] = location
221        
222         # then all the http headers
223         try:
224             self.read_headers()
225         except ValueError, ex:
226             self.simple_response("400 Bad Request", repr(ex.args))
227             return
228        
229         creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
230         environ["AUTH_TYPE"] = creds[0]
231         if creds[0].lower() == 'basic':
232             user, pw = base64.decodestring(creds[1]).split(":", 1)
233             environ["REMOTE_USER"] = user
234        
235         # Persistent connection support
236         if self.response_protocol == "HTTP/1.1":
237             if environ.get("HTTP_CONNECTION", "") == "close":
238                 self.close_connection = True
239         else:
240             # HTTP/1.0
241             if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
242                 self.close_connection = True
243        
244         # Transfer-Encoding support
245         te = None
246         if self.response_protocol == "HTTP/1.1":
247             te = environ.get("HTTP_TRANSFER_ENCODING")
248             if te:
249                 te = [x.strip().lower() for x in te.split(",") if x.strip()]
250        
251         read_chunked = False
252        
253         if te:
254             for enc in te:
255                 if enc == "chunked":
256                     read_chunked = True
257                 else:
258                     # Note that, even if we see "chunked", we must reject
259                     # if there is an extension we don't recognize.
260                     self.simple_response("501 Unimplemented")
261                     self.close_connection = True
262                     return
263        
264         if read_chunked:
265             if not self.decode_chunked():
266                 return
267         else:
268             cl = environ.get("CONTENT_LENGTH")
269             if method in ("POST", "PUT") and cl is None:
270                 # No Content-Length header supplied. This will hang
271                 # cgi.FieldStorage, since it cannot determine when to
272                 # stop reading from the socket.
273                 # See http://www.cherrypy.org/ticket/493.
274                 self.simple_response("411 Length Required")
275                 return
276        
277         # From PEP 333:
278         # "Servers and gateways that implement HTTP 1.1 must provide
279         # transparent support for HTTP 1.1's "expect/continue" mechanism.
280         # This may be done in any of several ways:
281         #   1. Respond to requests containing an Expect: 100-continue request
282         #      with an immediate "100 Continue" response, and proceed normally.
283         #   2. Proceed with the request normally, but provide the application
284         #      with a wsgi.input stream that will send the "100 Continue"
285         #      response if/when the application first attempts to read from
286         #      the input stream. The read request must then remain blocked
287         #      until the client responds.
288         #   3. Wait until the client decides that the server does not support
289         #      expect/continue, and sends the request body on its own.
290         #      (This is suboptimal, and is not recommended.)
291         #
292         # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
293         # but it seems like it would be a big slowdown for such a rare case.
294         if environ.get("HTTP_EXPECT", "") == "100-continue":
295             self.simple_response(100)
296        
297         self.ready = True
298    
299     def read_headers(self):
300         """Read header lines from the incoming stream."""
301         environ = self.environ
302        
303         while True:
304             line = self.rfile.readline()
305             if not line:
306                 # No more data--illegal end of headers
307                 raise ValueError("Illegal end of headers.")
308            
309             if line == '\r\n':
310                 # Normal end of headers
311                 break
312            
313             if line[0] in ' \t':
314                 # It's a continuation line.
315                 v = line.strip()
316             else:
317                 k, v = line.split(":", 1)
318                 k, v = k.strip().upper(), v.strip()
319                 envname = "HTTP_" + k.replace("-", "_")
320            
321             if k in comma_separated_headers:
322                 existing = environ.get(envname)
323                 if existing:
324                     v = ", ".join((existing, v))
325             environ[envname] = v
326        
327         ct = environ.pop("HTTP_CONTENT_TYPE", None)
328         if ct:
329             environ["CONTENT_TYPE"] = ct
330         cl = environ.pop("HTTP_CONTENT_LENGTH", None)
331         if cl:
332             environ["CONTENT_LENGTH"] = cl
333    
334     def decode_chunked(self):
335         """Decode the 'chunked' transfer coding."""
336         cl = 0
337         data = StringIO.StringIO()
338         while True:
339             line = self.rfile.readline().strip().split(";", 1)
340             chunk_size = int(line.pop(0), 16)
341             if chunk_size <= 0:
342                 break
343 ##            if line: chunk_extension = line[0]
344             cl += chunk_size
345             data.write(self.rfile.read(chunk_size))
346             crlf = self.rfile.read(2)
347             if crlf != "\r\n":
348                 self.simple_response("400 Bad Request",
349                                      "Bad chunked transfer coding "
350                                      "(expected '\\r\\n', got %r)" % crlf)
351                 return
352        
353         # Grab any trailer headers
354         self.read_headers()
355        
356         data.seek(0)
357         self.environ["wsgi.input"] = data
358         self.environ["CONTENT_LENGTH"] = str(cl) or ""
359         return True
360    
361     def respond(self):
362         """Call the appropriate WSGI app and write its iterable output."""
363         response = self.wsgi_app(self.environ, self.start_response)
364         try:
365             for chunk in response:
366                 self.write(chunk)
367         finally:
368             if hasattr(response, "close"):
369                 response.close()
370         if (self.ready and not self.sent_headers
371                 and not self.connection.server.interrupt):
372             self.sent_headers = True
373             self.send_headers()
374         if self.chunked_write:
375             self.sendall("0\r\n\r\n")
376    
377     def simple_response(self, status, msg=""):
378         """Write a simple response back to the client."""
379         status = str(status)
380         buf = ["%s %s\r\n" % (self.connection.server.protocol, status),
381                "Content-Length: %s\r\n" % len(msg)]
382        
383         if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
384             # Request Entity Too Large
385             self.close_connection = True
386             buf.append("Connection: close\r\n")
387        
388         buf.append("\r\n")
389         if msg:
390             buf.append(msg)
391         self.sendall("".join(buf))
392    
393     def start_response(self, status, headers, exc_info = None):
394         """WSGI callable to begin the HTTP response."""
395         if self.started_response:
396             if not exc_info:
397                 assert False, "Already started response"
398             else:
399                 try:
400                     raise exc_info[0], exc_info[1], exc_info[2]
401                 finally:
402                     exc_info = None
403         self.started_response = True
404         self.status = status
405         self.outheaders.extend(headers)
406         return self.write
407    
408     def write(self, chunk):
409         """WSGI callable to write unbuffered data to the client.
410         
411         This method is also used internally by start_response (to write
412         data from the iterable returned by the WSGI application).
413         """
414         if not self.sent_headers:
415             self.sent_headers = True
416             self.send_headers()
417         if self.chunked_write:
418             buf = [hex(len(chunk))[2:],
419                    "\r\n", chunk, "\r\n"]
420             self.sendall("".join(buf))
421         else:
422             self.sendall(chunk)
423    
424     def send_headers(self):
425         """Assert, process, and send the HTTP response message-headers."""
426         hkeys = [key.lower() for key, value in self.outheaders]
427         status = int(self.status[:3])
428        
429         if status == 413:
430             # Request Entity Too Large. Close conn to avoid garbage.
431             self.close_connection = True
432         elif "content-length" not in hkeys:
433             # "All 1xx (informational), 204 (no content),
434             # and 304 (not modified) responses MUST NOT
435             # include a message-body." So no point chunking.
436             if status < 200 or status in (204, 205, 304):
437                 pass
438             else:
439                 if self.response_protocol == 'HTTP/1.1':
440                     # Use the chunked transfer-coding
441                     self.chunked_write = True
442                     self.outheaders.append(("Transfer-Encoding", "chunked"))
443                 else:
444                     # Closing the conn is the only way to determine len.
445                     self.close_connection = True
446        
447         if "connection" not in hkeys:
448             if self.response_protocol == 'HTTP/1.1':
449                 if self.close_connection:
450                     self.outheaders.append(("Connection", "close"))
451             else:
452                 if not self.close_connection:
453                     self.outheaders.append(("Connection", "Keep-Alive"))
454        
455         if "date" not in hkeys:
456             self.outheaders.append(("Date", rfc822.formatdate()))
457        
458         server = self.connection.server
459        
460         if "server" not in hkeys:
461             self.outheaders.append(("Server", server.version))
462        
463         buf = [server.protocol, " ", self.status, "\r\n"]
464         try:
465             buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
466         except TypeError:
467             if not isinstance(k, str):
468                 raise TypeError("WSGI response header key %r is not a string.")
469             if not isinstance(v, str):
470                 raise TypeError("WSGI response header value %r is not a string.")
471             else:
472                 raise
473         buf.append("\r\n")
474         self.sendall("".join(buf))
475
476
477 def _ssl_wrap_method(method, is_reader=False):
478     """Wrap the given method with SSL error-trapping.
479     
480     is_reader: if False (the default), EOF errors will be raised.
481         If True, EOF errors will return "" (to emulate normal sockets).
482     """
483     def ssl_method_wrapper(self, *args, **kwargs):
484 ##        print (id(self), method, args, kwargs)
485         start = time.time()
486         while True:
487             try:
488                 return method(self, *args, **kwargs)
489             except (SSL.WantReadError, SSL.WantWriteError):
490                 # Sleep and try again. This is dangerous, because it means
491                 # the rest of the stack has no way of differentiating
492                 # between a "new handshake" error and "client dropped".
493                 # Note this isn't an endless loop: there's a timeout below.
494                 time.sleep(self.ssl_retry)
495             except SSL.SysCallError, e:
496                 if is_reader and e.args == (-1, 'Unexpected EOF'):
497                     return ""
498                
499                 errno = e.args[</