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

root/tags/cherrypy-3.0.0/cherrypy/wsgiserver.py

Revision 1555 (checked in by fumanchu, 3 years ago)

wsgiserver optimization: use socket.sendall instead of wfile (since we were flushing regularly anyway).

  • 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         self.environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version
144        
145         method, path, req_protocol = request_line.strip().split(" ", 2)
146         self.environ["REQUEST_METHOD"] = method
147        
148         # path may be an abs_path (including "http://host.domain.tld");
149         scheme, location, path, params, qs, frag = urlparse(path)
150        
151         if frag:
152             self.simple_response("400 Bad Request",
153                                  "Illegal #fragment in Request-URI.")
154             return
155        
156         if scheme:
157             self.environ["wsgi.url_scheme"] = scheme
158         if params:
159             path = path + ";" + params
160        
161         # Unquote the path+params (e.g. "/this%20path" -> "this path").
162         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
163         #
164         # But note that "...a URI must be separated into its components
165         # before the escaped characters within those components can be
166         # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
167         atoms = [unquote(x) for x in quoted_slash.split(path)]
168         path = "%2F".join(atoms)
169        
170         if path == "*":
171             # This means, of course, that the last wsgi_app (shortest path)
172             # will always handle a URI of "*".
173             self.environ["SCRIPT_NAME"] = ""
174             self.environ["PATH_INFO"] = "*"
175             self.wsgi_app = server.mount_points[-1][1]
176         else:
177             for mount_point, wsgi_app in server.mount_points:
178                 # The mount_points list should be sorted by length, descending.
179                 if path.startswith(mount_point + "/") or path == mount_point:
180                     self.environ["SCRIPT_NAME"] = mount_point
181                     self.environ["PATH_INFO"] = path[len(mount_point):]
182                     self.wsgi_app = wsgi_app
183                     break
184             else:
185                 self.simple_response("404 Not Found")
186                 return
187        
188         # Note that, like wsgiref and most other WSGI servers,
189         # we unquote the path but not the query string.
190         self.environ["QUERY_STRING"] = qs
191        
192         # Compare request and server HTTP protocol versions, in case our
193         # server does not support the requested protocol. Limit our output
194         # to min(req, server). We want the following output:
195         #     request    server     actual written   supported response
196         #     protocol   protocol  response protocol    feature set
197         # a     1.0        1.0           1.0                1.0
198         # b     1.0        1.1           1.1                1.0
199         # c     1.1        1.0           1.0                1.0
200         # d     1.1        1.1           1.1                1.1
201         # Notice that, in (b), the response will be "HTTP/1.1" even though
202         # the client only understands 1.0. RFC 2616 10.5.6 says we should
203         # only return 505 if the _major_ version is different.
204         rp = int(req_protocol[5]), int(req_protocol[7])
205         sp = int(server.protocol[5]), int(server.protocol[7])
206         if sp[0] != rp[0]:
207             self.simple_response("505 HTTP Version Not Supported")
208             return
209         # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
210         self.environ["SERVER_PROTOCOL"] = req_protocol
211         # set a non-standard environ entry so the WSGI app can know what
212         # the *real* server protocol is (and what features to support).
213         # See http://www.faqs.org/rfcs/rfc2145.html.
214         self.environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol
215         self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
216        
217         # If the Request-URI was an absoluteURI, use its location atom.
218         if location:
219             self.environ["SERVER_NAME"] = location
220        
221         # then all the http headers
222         headers = rfc822.Message(self.rfile, seekable=0)
223         self.environ.update(self.parse_headers(headers))
224        
225         creds = headers.getheader("Authorization", "").split(" ", 1)
226         self.environ["AUTH_TYPE"] = creds[0]
227         if creds[0].lower() == 'basic':
228             user, pw = base64.decodestring(creds[1]).split(":", 1)
229             self.environ["REMOTE_USER"] = user
230        
231         # Persistent connection support
232         if self.response_protocol == "HTTP/1.1":
233             if headers.getheader("Connection", "") == "close":
234                 self.close_connection = True
235                 self.outheaders.append(("Connection", "close"))
236         else:
237             # HTTP/1.0
238             if headers.getheader("Connection", "") == "Keep-Alive":
239                 if self.close_connection == False:
240                     self.outheaders.append(("Connection", "Keep-Alive"))
241             else:
242                 self.close_connection = True
243        
244         # Transfer-Encoding support
245         te = headers.getheader("Transfer-Encoding", "")
246         te = [x.strip() for x in te.split(",") if x.strip()]
247         if te:
248             while te:
249                 enc = te.pop()
250                 if enc.lower() == "chunked":
251                     if not self.decode_chunked():
252                         return
253                 else:
254                     self.simple_response("501 Unimplemented")
255                     self.close_connection = True
256                     return
257         else:
258             cl = headers.getheader("Content-length")
259             if method in ("POST", "PUT") and cl is None:
260                 # No Content-Length header supplied. This will hang
261                 # cgi.FieldStorage, since it cannot determine when to
262                 # stop reading from the socket.
263                 # See http://www.cherrypy.org/ticket/493.
264                 self.simple_response("411 Length Required")
265                 return
266        
267         # From PEP 333:
268         # "Servers and gateways that implement HTTP 1.1 must provide
269         # transparent support for HTTP 1.1's "expect/continue" mechanism.
270         # This may be done in any of several ways:
271         #   1. Respond to requests containing an Expect: 100-continue request
272         #      with an immediate "100 Continue" response, and proceed normally.
273         #   2. Proceed with the request normally, but provide the application
274         #      with a wsgi.input stream that will send the "100 Continue"
275         #      response if/when the application first attempts to read from
276         #      the input stream. The read request must then remain blocked
277         #      until the client responds.
278         #   3. Wait until the client decides that the server does not support
279         #      expect/continue, and sends the request body on its own.
280         #      (This is suboptimal, and is not recommended.)
281         #
282         # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
283         # but it seems like it would be a big slowdown for such a rare case.
284         if headers.getheader("Expect", "") == "100-continue":
285             self.simple_response(100)
286        
287         self.ready = True
288    
289     def parse_headers(self, headers):
290         """Parse the given HTTP request message-headers."""
291         environ = {}
292         ct = headers.dict.get("content-type")
293         if ct:
294             environ["CONTENT_TYPE"] = ct
295         cl = headers.dict.get("content-length")
296         if cl:
297             environ["CONTENT_LENGTH"] = cl
298        
299         for line in headers.headers:
300             if line[:1].isspace():
301                 v = line.strip()
302             else:
303                 k, v = line.split(":", 1)
304                 k, v = k.strip().upper(), v.strip()
305                 envname = "HTTP_" + k.replace("-", "_")
306            
307             if k in comma_separated_headers:
308                 existing = environ.get(envname)
309                 if existing:
310                     v = ", ".join((existing, v))
311             environ[envname] = v
312        
313         return environ
314    
315     def decode_chunked(self):
316         """Decode the 'chunked' transfer coding."""
317         cl = 0
318         data = StringIO.StringIO()
319         while True:
320             line = self.rfile.readline().strip().split(";", 1)
321             chunk_size = int(line.pop(0), 16)
322             if chunk_size <= 0:
323                 break
324 ##            if line: chunk_extension = line[0]
325             cl += chunk_size
326             data.write(self.rfile.read(chunk_size))
327             crlf = self.rfile.read(2)
328             if crlf != "\r\n":
329                 self.simple_response("400 Bad Request",
330                                      "Bad chunked transfer coding "
331                                      "(expected '\\r\\n', got %r)" % crlf)
332                 return
333        
334         # Grab any trailer headers
335         headers = rfc822.Message(self.rfile, seekable=0)
336         self.environ.update(self.parse_headers(headers))
337        
338         data.seek(0)
339         self.environ["wsgi.input"] = data
340         self.environ["CONTENT_LENGTH"] = str(cl) or ""
341         return True
342    
343     def respond(self):
344         """Call the appropriate WSGI app and write its iterable output."""
345         response = self.wsgi_app(self.environ, self.start_response)
346         try:
347             for chunk in response:
348                 self.write(chunk)
349         finally:
350             if hasattr(response, "close"):
351                 response.close()
352         if (self.ready and not self.sent_headers
353                 and not self.connection.server.interrupt):
354             self.sent_headers = True
355             self.send_headers()
356         if self.chunked_write:
357             self.sendall("0\r\n\r\n")
358    
359     def simple_response(self, status, msg=""):
360         """Write a simple response back to the client."""
361         status = str(status)
362         buf = ["%s %s\r\n" % (self.connection.server.protocol, status),
363                "Content-Length: %s\r\n" % len(msg)]
364        
365         if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
366             # Request Entity Too Large
367             self.close_connection = True
368             buf.append("Connection: close\r\n")
369        
370         buf.append("\r\n")
371         if msg:
372             buf.append(msg)
373         self.sendall("".join(buf))
374    
375     def start_response(self, status, headers, exc_info = None):
376         """WSGI callable to begin the HTTP response."""
377         if self.started_response:
378             if not exc_info:
379                 assert False, "Already started response"
380             else:
381                 try:
382                     raise exc_info[0], exc_info[1], exc_info[2]
383                 finally:
384                     exc_info = None
385         self.started_response = True
386         self.status = status
387         self.outheaders.extend(headers)
388         return self.write
389    
390     def write(self, chunk):
391         """WSGI callable to write unbuffered data to the client.
392         
393         This method is also used internally by start_response (to write
394         data from the iterable returned by the WSGI application).
395         """
396         if not self.sent_headers:
397             self.sent_headers = True
398             self.send_headers()
399         if self.chunked_write:
400             buf = [hex(len(chunk))[2:],
401                    "\r\n", chunk, "\r\n"]
402             self.sendall("".join(buf))
403         else:
404             self.sendall(chunk)
405    
406     def send_headers(self):
407         """Assert, process, and send the HTTP response message-headers."""
408         hkeys = [key.lower() for (key, value) in self.outheaders]
409         status = int(self.status[:3])
410        
411         if self.response_protocol == 'HTTP/1.1':
412             if status == 413:
413                 # Request Entity Too Large. Close conn to avoid garbage.
414                 self.close_connection = True
415             elif "content-length" not in hkeys:
416                 # "All 1xx (informational), 204 (no content),
417                 # and 304 (not modified) responses MUST NOT
418                 # include a message-body." So no point chunking.
419                 if status < 200 or status in (204, 205, 304):
420                     pass
421                 else:
422                     # Use the chunked transfer-coding
423                     self.chunked_write = True
424                     self.outheaders.append(("Transfer-Encoding", "chunked"))
425        
426         if self.close_connection and "connection" not in hkeys:
427             self.outheaders.append(("Connection", "close"))
428        
429         if "date" not in hkeys:
430             self.outheaders.append(("Date", rfc822.formatdate()))
431        
432         server = self.connection.server
433        
434         if "server" not in hkeys:
435             self.outheaders.append(("Server", server.version))
436        
437         buf = [server.protocol, " ", self.status, "\r\n"]
438         try:
439             for k, v in self.outheaders:
440                 buf.append(k + ": " + v + "\r\n")
441         except TypeError:
442             if not isinstance(k, str):
443                 raise TypeError("WSGI response header key %r is not a string.")
444             if not isinstance(v, str):
445                 raise TypeError("WSGI response header value %r is not a string.")
446             else:
447                 raise
448         buf.append("\r\n")
449         self.sendall("".join(buf))
450
451
452 def _ssl_wrap_method(method, is_reader=False):
453     """Wrap the given method with SSL error-trapping.
454     
455     is_reader: if False (the default), EOF errors will be raised.
456         If True, EOF errors will return "" (to emulate normal sockets).
457     """
458     def ssl_method_wrapper(self, *args, **kwargs):
459 ##        print (id(self), method, args, kwargs)
460         start = time.time()
461         while True:
462             try:
463                 return method(self, *args, **kwargs)
464             except (SSL.WantReadError, SSL.WantWriteError):
465                 # Sleep and try again. This is dangerous, because it means
466                 # the rest of the stack has no way of differentiating
467                 # between a "new handshake" error and "client dropped".
468                 # Note this isn't an endless loop: there's a timeout below.
469                 time.sleep(self.ssl_retry)
470             except SSL.SysCallError, e:
471                 if is_reader and e.args == (-1, 'Unexpected EOF'):
472                     return ""
473                
474                 errno = e.args[0]
475                 if is_reader and errno in socket_errors_to_ignore:
476                     return ""
477                 raise socket.error(errno)
478             except SSL.Error, e:
479                 if is_reader and e.args == (-1, 'Unexpected EOF'):
480                     return ""
481                 if is_reader and e.args[0][0][2] == 'ssl handshake failure':
482                     return ""
483                 raise
484             if time.time() - start > self.ssl_timeout:
485                 raise socket.timeout("timed out")
486     return ssl_method_wrapper
487
488 class SSL_fileobject(socket._fileobject):
489     """Faux file object attached to a socket object."""
490    
491     ssl_timeout = 3
492     ssl_retry = .01
493    
494     close = _ssl_wrap_method(socket._fileobject.close)
495     flush = _ssl_wrap_method(socket._fileobject.flush)
496     write = _ssl_wrap_method(socket._fileobject.write)
497     writelines = _ssl_wrap_method(socket._fileobject.writelines)
498     read = _ssl_wrap_method(socket._fileobject.read, is_reader=True)
499     readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True)
500     readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True)
501
502
503 class HTTPConnection(object):
504     """An HTTP connection (active socket).
505     
506     socket: the raw socket object (usually TCP) for this connection.
507     addr: the "bind address" for the remote end of the socket.
508         For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT).
509         For UNIX domain sockets, this will be a string.
510     server: the HTTP Server for this Connection. Usually, the server
511         object possesses a passive (server) socket which spawns multiple,
512         active (client) sockets, one for each connection.
513     
514     environ: a WSGI environ template. This will be copied for each request.
515     rfile: a fileobject for reading from the socket.
516     sendall: a function for writing (+ flush) to the socket.
517     """
518    
519     rbufsize = -1
520     RequestHandlerClass = HTTPRequest
521     environ = {"wsgi.version": (1, 0),
522                "wsgi.url_scheme": "http",
523                "wsgi.multithread": True,
524                "wsgi.multiprocess": False,
525                "wsgi.run_once": False,
526                "wsgi.errors": sys.stderr,
527                }
528    
529     def __init__(self, sock, addr, server):
530         self.socket = sock
531         self.addr = addr
532         self.server = server
533        
534         # Copy the class environ into self.
535         self.environ = self.environ.copy()
536        
537         if SSL and isinstance(sock, SSL.ConnectionType):
538             timeout = sock.gettimeout()
539             self.rfile = SSL_fileobject(sock, "r", self.rbufsize)
540             self.rfile.ssl_timeout = timeout
541             self.sendall = _ssl_wrap_method(sock.sendall)
542             self.environ["wsgi.url_scheme"] = "https"
543             self.environ["HTTPS"] = "on"
544             sslenv = getattr(server, "ssl_environ", None)
545             if sslenv:
546                 self.environ.update(sslenv)
547         else:
548             self.rfile = sock.makefile("r", self.rbufsize)
549             self.sendall = sock.sendall
550        
551         self.environ.update({"wsgi.input": self.rfile,
552                              "SERVER_NAME": self.server.server_name,
553                              })
554        
555         if isinstance(self.server.bind_addr, basestring):
556             # AF_UNIX. This isn't really allowed by WSGI, which doesn't
557             # address unix domain sockets. But it's better than nothing.
558             self.environ["SERVER_PORT"] = ""
559         else:
560             self.environ["SERVER_PORT"] = str(self.server.bind_addr[1])
561             # optional values
562             # Until we do DNS lookups, omit REMOTE_HOST
563             self.environ["REMOTE_ADDR"] = self.addr[0]
564             self.environ["REMOTE_PORT"] = str(self.addr[1])
565    
566     def communicate(self):
567         """Read each request and respond appropriately."""
568         try:
569             while True:
570                 # (re)set req to None so that if something goes wrong in
571                 # the RequestHandlerClass constructor, the error doesn't
572                 # get written to the previous request.
573                 req = None
574                 req = self.RequestHandlerClass(self)
575                 # This order of operations should guarantee correct pipelining.
576                 req.parse_request()
577                 if not req.ready:
578                     return
579                 req.respond()
580                 if req.close_connection:
581                     return
582         except socket.error, e:
583             errno = e.args[0]
584             if errno not in socket_errors_to_ignore:
585                 if req:
586                     req.simple_response("500 Internal Server Error",
587                                         format_exc())
588             return
589         except (KeyboardInterrupt, SystemExit):
590             raise
591         except:
592             if req:
593                 req.simple_response("500 Internal Server Error", format_exc())
594    
595     def close(self):
596         """Close the socket underlying this connection."""
597         self.rfile.close()
598         self.socket.close()
599
600
601 def format_exc(limit=None):
602     """Like print_exc() but return a string. Backport for Python 2.3."""
603     try:
604         etype, value, tb = sys.exc_info()
605         return ''.join(traceback.format_exception(etype, value, tb, limit))
606     finally:
607         etype = value = tb = None
608
609
610 _SHUTDOWNREQUEST = None
611
612 class WorkerThread(threading.Thread):
613     """Thread which continuously polls a Queue for Connection objects.
614     
615     server: the HTTP Server which spawned this thread, and which owns the
616         Queue and is placing active connections into it.
617     ready: a simple flag for the calling server to know when this thread
618         has begun polling the Queue.
619     
620     Due to the timing issues of polling a Queue, a WorkerThread does not
621     check its own 'ready' flag after it has started. To stop the thread,
622     it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
623     (one for each running WorkerThread).
624     """
625    
626     def __init__(self, server):
627         self.ready = False
628         self.server = server
629         threading.Thread.__init__(self)
630    
631     def run(self):
632         try:
633             self.ready = True
634             while True:
635                 conn = self.server.requests.get()
636                 if conn is _SHUTDOWNREQUEST:
637                     return
638                
639                 try:
640                     conn.communicate()
641                 finally:
642                     conn.close()
643         except (KeyboardInterrupt, SystemExit), exc:
644             self.server.interrupt = exc
645
646
647 class SSLConnection:
648     """A thread-safe wrapper for an SSL.Connection.
649     
650     *args: the arguments to create the wrapped SSL.Connection(*args).
651     """
652    
653     def __init__(self, *args):
654         self._ssl_conn = SSL.Connection(*args)
655         self._lock = threading.RLock()
656    
657     for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
658               'renegotiate', 'bind', 'listen', 'connect', 'accept',
659               'setblocking', 'fileno', 'shutdown', 'close', 'get_cipher_list',
660               'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
661               'makefile', 'get_app_data', 'set_app_data', 'state_string',
662               'sock_shutdown', 'get_peer_certificate', 'want_read',
663               'want_write', 'set_connect_state', 'set_accept_state',
664               'connect_ex', 'sendall', 'settimeout'):
665         exec """def %s(self, *args):
666         self._lock.acquire()
667         try:
668             return self._ssl_conn.%s(*args)
669         finally:
670             self._lock.release()
671 """ % (f, f)
672
673
674 class CherryPyWSGIServer(object):
675     """An HTTP server for WSGI.
676     
677     bind_addr: a (host, port) tuple if TCP sockets are desired;
678         for UNIX sockets, supply the filename as a string.
679     wsgi_app: the WSGI 'application callable'; multiple WSGI applications
680         may be passed as (script_name, callable) pairs.
681     numthreads: the number of worker threads to create (default 10).
682     server_name: the string to set for WSGI's SERVER_NAME environ entry.
683         Defaults to socket.gethostname().
684     max: the maximum number of queued requests (defaults to -1 = no limit).
685     request_queue_size: the 'backlog' argument to socket.listen();
686         specifies the maximum number of queued connections (default 5).
687     timeout: the timeout in seconds for accepted connections (default 10).
688     
689     protocol: the version string to write in the Status-Line of all
690         HTTP responses. For example, "HTTP/1.1" (the default). This
691         also limits the supported features used in the response.
692     
693     
694     SSL/HTTPS
695     ---------
696     The OpenSSL module must be importable for SSL functionality.
697     You can obtain it from http://pyopenssl.sourceforge.net/
698     
699     ssl_certificate: the filename of the server SSL certificate.
700     ssl_privatekey: the filename of the server's private key file.
701     
702     If either of these is None (both are None by default), this server
703     will not use SSL. If both are given and are valid, they will be read
704     on server start and used in the SSL context for the listening socket.
705     """
706    
707     protocol = "HTTP/1.1"
708     version = "CherryPy/3.0.0"
709     ready = False
710     _interrupt = None
711     ConnectionClass = HTTPConnection
712    
713     # Paths to certificate and private key files
714     ssl_certificate = None
715     ssl_private_key = None
716    
717     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
718                  max=-1, request_queue_size=5, timeout=10):
719         self.requests = Queue.Queue(max)
720        
721         if callable(wsgi_app):
722             # We've been handed a single wsgi_app, in CP-2.1 style.
723             # Assume it's mounted at "".
724             self.mount_points = [("", wsgi_app)]
725         else:
726             # We've been handed a list of (mount_point, wsgi_app) tuples,
727             # so that the server can call different wsgi_apps, and also
728             # correctly set SCRIPT_NAME.
729             self.mount_points = wsgi_app
730         self.mount_points.sort()
731         self.mount_points.reverse()
732        
733         self.bind_addr = bind_addr
734         self.numthreads = numthreads or 1
735         if not server_name:
736             server_name = socket.gethostname()
737         self.server_name = server_name
738         self.request_queue_size = request_queue_size
739         self._workerThreads = []
740        
741         self.timeout = timeout
742    
743     def start(self):
744         """Run the server forever."""
745         # We don't have to trap KeyboardInterrupt or SystemExit here,
746         # because cherrpy.server already does so, calling self.stop() for us.
747         # If you're using this server with another framework, you should
748         # trap those exceptions in whatever code block calls start().
749         self._interrupt = None
750        
751         def bind(family, type, proto=0):
752             """Create (or recreate) the actual socket object."""
753             self.socket = socket.socket(family, type, proto)
754             self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
755             if self.ssl_certificate and self.ssl_private_key:
756                 if SSL is None:
757                     raise ImportError("You must install pyOpenSSL to use HTTPS.")
758                
759                 # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
760                 ctx = SSL.Context(SSL.SSLv23_METHOD)
761                 ctx.use_privatekey_file(self.ssl_private_key)
762                 ctx.use_certificate_file(self.ssl_certificate)
763                 self.socket = SSLConnection(ctx, self.socket)
764                 self.populate_ssl_environ()
765             self.socket.bind(self.bind_addr)
766        
767         # Select the appropriate socket
768         if isinstance(self.bind_addr, basestring):
769             # AF_UNIX socket
770            
771             # So we can reuse the socket...
772             try: os.unlink(self.bind_addr)
773             except: pass
774            
775             # So everyone can access the socket...
776             try: os.chmod(self.bind_addr, 0777)
777             except: pass
778            
779             info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
780         else:
781             # AF_INET or AF_INET6 socket
782             # Get the correct address family for our host (allows IPv6 addresses)
783             host, port = self.bind_addr
784             flags = 0
785             if host == '':
786                 # Despite the socket module docs, using '' does not
787                 # allow AI_PASSIVE to work. Passing None instead
788                 # returns '0.0.0.0' like we want. In other words:
789                 #     host    AI_PASSIVE     result
790                 #      ''         Y         192.168.x.y
791                 #      ''         N         192.168.x.y
792                 #     None        Y         0.0.0.0
793                 #     None        N         127.0.0.1
794                 host = None
795                 flags = socket.AI_PASSIVE
796             try:
797                 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
798                                           socket.SOCK_STREAM, 0, flags)
799             except socket.gaierror:
800                 # Probably a DNS issue. Assume IPv4.
801                 info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)]
802        
803         self.socket = None
804         msg = "No socket could be created"
805         for res in info:
806             af, socktype, proto, canonname, sa = res
807             try:
808                 bind(af, socktype, proto)
809             except socket.error, msg:
810                 if self.socket:
811                     self.socket.close()
812                 self.socket = None
813                 continue
814             break
815         if not self.socket:
816             raise socket.error, msg
817        
818         # Timeout so KeyboardInterrupt can be caught on Win32
819         self.socket.settimeout(1)
820         self.socket.listen(self.request_queue_size)
821        
822         # Create worker threads
823         for i in xrange(self.numthreads):
824             self._workerThreads.append(WorkerThread(self))
825         for worker in self._workerThreads:
826             worker.setName("CP WSGIServer " + worker.getName())
827             worker.start()
828         for worker in self._workerThreads:
829             while not worker.ready:
830                 time.sleep(.1)
831        
832         self.ready = True
833         while self.ready:
834             self.tick()
835             if self.interrupt:
836                 while self.interrupt is True:
837                     # Wait for self.stop() to complete. See _set_interrupt.
838                     time.sleep(0.1)
839                 raise self.interrupt
840    
841     def tick(self):
842         """Accept a new connection and put it on the Queue."""
843         try:
844             s, addr = self.socket.accept()
845             if not self.ready:
846                 return
847             if hasattr(s, 'settimeout'):
848                 s.settimeout(self.timeout)
849             conn = self.ConnectionClass(s, addr, self)
850             self.requests.put(conn)
851         except socket.timeout:
852             # The only reason for the timeout in start() is so we can
853             # notice keyboard interrupts on Win32, which don't interrupt
854             # accept() by default
855             return
856         except socket.error, x:
857             msg = x.args[1]
858             if msg in ("Bad file descriptor", "Socket operation on non-socket"):
859                 # Our socket was closed.
860                 return
861             if msg == "Resource temporarily unavailable":
862                 # Just try again. See http://www.cherrypy.org/ticket/479.
863                 return
864             raise
865    
866     def _get_interrupt(self):
867         return self._interrupt
868     def _set_interrupt(self, interrupt):
869         self._interrupt = True
870         self.stop()
871         self._interrupt = interrupt
872     interrupt = property(_get_interrupt, _set_interrupt,
873                          doc="Set this to an Exception instance to "
874                              "interrupt the server.")
875    
876     def stop(self):
877         """Gracefully shutdown a server that is serving forever."""
878         self.ready = False
879        
880         sock = getattr(self, "socket", None)
881         if sock:
882             if not isinstance(self.bind_addr, basestring):
883                 # Touch our own socket to make accept() return immediately.
884                 try:
885                     host, port = sock.getsockname()[:2]
886                 except socket.error, x:
887                     if x.args[1] != "Bad file descriptor":
888                         raise
889                 else:
890                     # Note that we're explicitly NOT using AI_PASSIVE,
891                     # here, because we want an actual IP to touch.
892                     # localhost won't work if we've bound to a public IP,
893                     # but it would if we bound to INADDR_ANY via host = ''.
894                     for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
895                                                   socket.SOCK_STREAM):
896                         af, socktype, proto, canonname, sa = res
897                         s = None
898                         try:
899                             s = socket.socket(af, socktype, proto)
900                             # See http://groups.google.com/group/cherrypy-users/
901                             #        browse_frm/thread/bbfe5eb39c904fe0
902                             s.settimeout(1.0)
903                             s.connect((host, port))
904                             s.close()
905                         except socket.error:
906                             if s:
907                                 s.close()
908             if hasattr(sock, "close"):
909                 sock.close()
910             self.socket = None
911        
912         # Must shut down threads here so the code that calls
913         # this method can know when all threads are stopped.
914         for worker in self._workerThreads:
915             self.requests.put(_SHUTDOWNREQUEST)
916        
917         # Don't join currentThread (when stop is called inside a request).
918         current = threading.currentThread()
919         while self._workerThreads:
920             worker = self._workerThreads.pop()
921             if worker is not current and worker.isAlive:
922                 try:
923                     worker.join()
924                 except AssertionError:
925                     pass
926    
927     def populate_ssl_environ(self):
928         """Create WSGI environ entries to be merged into each request."""
929         cert = open(self.ssl_certificate).read()
930         cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
931         self.ssl_environ = {
932             # pyOpenSSL doesn't provide access to any of these AFAICT
933 ##            'SSL_PROTOCOL': 'SSLv2',
934 ##            SSL_CIPHER        string  The cipher specification name
935 ##            SSL_VERSION_INTERFACE     string  The mod_ssl program version
936 ##            SSL_VERSION_LIBRARY       string  The OpenSSL program version
937             }
938        
939         # Server certificate attributes
940         self.ssl_environ.update({
941             'SSL_SERVER_M_VERSION': cert.get_version(),
942             'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
943 ##            'SSL_SERVER_V_START': Validity of server's certificate (start time),
944 ##            'SSL_SERVER_V_END': Validity of server's certificate (end time),
945             })
946        
947         for prefix, dn in [("I", cert.get_issuer()),
948                            ("S", cert.get_subject())]:
949             # X509Name objects don't seem to have a way to get the
950             # complete DN string. Use str() and slice it instead,
951             # because str(dn) == "<X509Name object '/C=US/ST=...'>"
952             dnstr = str(dn)[18:-2]
953            
954             wsgikey = 'SSL_SERVER_%s_DN' % prefix
955             self.ssl_environ[wsgikey] = dnstr
956            
957             # The DN should be of the form: /k1=v1/k2=v2, but we must allow
958             # for any value to contain slashes itself (in a URL).
959             while dnstr:
960                 pos = dnstr.rfind("=")
961                 dnstr, value = dnstr[:pos], dnstr[pos + 1:]
962                 pos = dnstr.rfind("/")
963                 dnstr, key = dnstr[:pos], dnstr[pos + 1:]
964                 if key and value:
965                     wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
966                     self.ssl_environ[wsgikey] = value
967
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets