diff options
author | Robert Brewer <fumanchu@aminus.org> | 2006-12-28 20:10:07 +0000 |
---|---|---|
committer | Robert Brewer <fumanchu@aminus.org> | 2006-12-28 20:10:07 +0000 |
commit | e0078b45167db6f6927011140753bebe602b157d (patch) | |
tree | 8e11ebc2a6f0280e8804030345015b92619182fd | |
parent | 66befe917c1158ce2a4a48f5372cba52535ffe96 (diff) | |
download | cherrypy-e0078b45167db6f6927011140753bebe602b157d.tar.gz |
Copied recent changes from trunk to _cpwsgiserver3.
-rw-r--r-- | cherrypy/_cpwsgiserver3.py | 456 |
1 files changed, 309 insertions, 147 deletions
diff --git a/cherrypy/_cpwsgiserver3.py b/cherrypy/_cpwsgiserver3.py index 6e76dd1d..7cce66ff 100644 --- a/cherrypy/_cpwsgiserver3.py +++ b/cherrypy/_cpwsgiserver3.py @@ -1,7 +1,43 @@ -"""A high-speed, production ready, thread pooled, generic WSGI server.""" +"""A high-speed, production ready, thread pooled, generic WSGI server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery): + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!\n'] + + # Here we set our application to the script_name '/' + wsgi_apps = [('/', my_crazy_app)] + + server = wsgiserver.CherryPyWSGIServer(('localhost', 8070), wsgi_apps, + server_name='localhost') + + # Want SSL support? Just set these attributes + # server.ssl_certificate = <filename> + # server.ssl_private_key = <filename> + + if __name__ == '__main__': + server.start() + +This won't call the CherryPy engine (application side) at all, only the +WSGI server, which is independant from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not it's coupling. + +The CherryPy WSGI server can serve as many WSGI application +as you want in one instance: + + wsgi_apps = [('/', my_crazy_app), (/blog', my_blog_app)] + +""" + import base64 -import mimetools # todo: use email import Queue import os import re @@ -38,21 +74,36 @@ for _ in ("EPIPE", "ETIMEDOUT", "ECONNREFUSED", "ECONNRESET", socket_errors_to_ignore = dict.fromkeys(socket_errors_to_ignore).keys() socket_errors_to_ignore.append("timed out") -# These are lowercase because mimetools.Message uses lowercase keys. -comma_separated_headers = [ - 'accept', 'accept-charset', 'accept-encoding', 'accept-language', - 'accept-ranges', 'allow', 'cache-control', 'connection', 'content-encoding', - 'content-language', 'expect', 'if-match', 'if-none-match', 'pragma', - 'proxy-authenticate', 'te', 'trailer', 'transfer-encoding', 'upgrade', - 'vary', 'via', 'warning', 'www-authenticate', - ] - +comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING', + 'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL', + 'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT', + 'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE', + 'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING', + 'WWW-AUTHENTICATE'] class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + + connection: the HTTP Connection object which spawned this request. + rfile: the 'read' fileobject from the connection's socket + ready: when True, the request has been parsed and is ready to begin + generating the response. When False, signals the calling Connection + that the response should not be generated and the connection should + close. + close_connection: signals the calling Connection that the request + should close. This does not imply an error! The client and/or + server may each request that the connection be closed. + chunked_write: if True, output will be encoded with the "chunked" + transfer-coding. This value is set automatically inside + send_headers. + """ def __init__(self, connection): self.connection = connection self.rfile = self.connection.rfile + self.sendall = self.connection.sendall self.environ = connection.environ.copy() self.ready = False @@ -64,6 +115,7 @@ class HTTPRequest(object): self.chunked_write = False def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" # HTTP/1.1 connections are persistent by default. If a client # requests a page, then idles (leaves the connection open), # then rfile.readline() will raise socket.error("timed out"). @@ -77,11 +129,22 @@ class HTTPRequest(object): self.ready = False return + if request_line == "\r\n": + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + self.ready = False + return + server = self.connection.server - self.environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version + environ = self.environ + environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version method, path, req_protocol = request_line.strip().split(" ", 2) - self.environ["REQUEST_METHOD"] = method + environ["REQUEST_METHOD"] = method # path may be an abs_path (including "http://host.domain.tld"); scheme, location, path, params, qs, frag = urlparse(path) @@ -92,7 +155,7 @@ class HTTPRequest(object): return if scheme: - self.environ["wsgi.url_scheme"] = scheme + environ["wsgi.url_scheme"] = scheme if params: path = path + ";" + params @@ -108,15 +171,15 @@ class HTTPRequest(object): if path == "*": # This means, of course, that the last wsgi_app (shortest path) # will always handle a URI of "*". - self.environ["SCRIPT_NAME"] = "" - self.environ["PATH_INFO"] = "*" + environ["SCRIPT_NAME"] = "" + environ["PATH_INFO"] = "*" self.wsgi_app = server.mount_points[-1][1] else: for mount_point, wsgi_app in server.mount_points: # The mount_points list should be sorted by length, descending. if path.startswith(mount_point + "/") or path == mount_point: - self.environ["SCRIPT_NAME"] = mount_point - self.environ["PATH_INFO"] = path[len(mount_point):] + environ["SCRIPT_NAME"] = mount_point + environ["PATH_INFO"] = path[len(mount_point):] self.wsgi_app = wsgi_app break else: @@ -125,7 +188,7 @@ class HTTPRequest(object): # Note that, like wsgiref and most other WSGI servers, # we unquote the path but not the query string. - self.environ["QUERY_STRING"] = qs + environ["QUERY_STRING"] = qs # Compare request and server HTTP protocol versions, in case our # server does not support the requested protocol. Limit our output @@ -145,55 +208,64 @@ class HTTPRequest(object): self.simple_response("505 HTTP Version Not Supported") return # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. - self.environ["SERVER_PROTOCOL"] = req_protocol + environ["SERVER_PROTOCOL"] = req_protocol # set a non-standard environ entry so the WSGI app can know what # the *real* server protocol is (and what features to support). # See http://www.faqs.org/rfcs/rfc2145.html. - self.environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol + environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol self.response_protocol = "HTTP/%s.%s" % min(rp, sp) # If the Request-URI was an absoluteURI, use its location atom. if location: - self.environ["SERVER_NAME"] = location + environ["SERVER_NAME"] = location # then all the http headers - headers = mimetools.Message(self.rfile) - self.environ.update(self.parse_headers(headers)) + try: + self.read_headers() + except ValueError, ex: + self.simple_response("400 Bad Request", repr(ex.args)) + return - creds = headers.getheader("Authorization", "").split(" ", 1) - self.environ["AUTH_TYPE"] = creds[0] + creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1) + environ["AUTH_TYPE"] = creds[0] if creds[0].lower() == 'basic': user, pw = base64.decodestring(creds[1]).split(":", 1) - self.environ["REMOTE_USER"] = user + environ["REMOTE_USER"] = user # Persistent connection support if self.response_protocol == "HTTP/1.1": - if headers.getheader("Connection", "") == "close": + if environ.get("HTTP_CONNECTION", "") == "close": self.close_connection = True - self.outheaders.append(("Connection", "close")) else: # HTTP/1.0 - if headers.getheader("Connection", "") == "Keep-Alive": - if self.close_connection == False: - self.outheaders.append(("Connection", "Keep-Alive")) - else: + if environ.get("HTTP_CONNECTION", "") != "Keep-Alive": self.close_connection = True # Transfer-Encoding support - te = headers.getheader("Transfer-Encoding", "") - te = [x.strip() for x in te.split(",") if x.strip()] + te = None + if self.response_protocol == "HTTP/1.1": + te = environ.get("HTTP_TRANSFER_ENCODING") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + read_chunked = False + if te: - while te: - enc = te.pop() - if enc.lower() == "chunked": - if not self.decode_chunked(): - return + for enc in te: + if enc == "chunked": + read_chunked = True else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. self.simple_response("501 Unimplemented") self.close_connection = True return + + if read_chunked: + if not self.decode_chunked(): + return else: - cl = headers.getheader("Content-length") + cl = environ.get("CONTENT_LENGTH") if method in ("POST", "PUT") and cl is None: # No Content-Length header supplied. This will hang # cgi.FieldStorage, since it cannot determine when to @@ -219,35 +291,45 @@ class HTTPRequest(object): # # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, # but it seems like it would be a big slowdown for such a rare case. - if headers.getheader("Expect", "") == "100-continue": + if environ.get("HTTP_EXPECT", "") == "100-continue": self.simple_response(100) self.ready = True - def parse_headers(self, headers): - environ = {} - ct = headers.getheader("Content-type", "") - if ct: - environ["CONTENT_TYPE"] = ct - cl = headers.getheader("Content-length") or "" - if cl: - environ["CONTENT_LENGTH"] = cl + def read_headers(self): + """Read header lines from the incoming stream.""" + environ = self.environ - # Must use keys() here for Python 2.3 (rfc822.Message had no __iter__). - for k in headers.keys(): - if k in ('transfer-encoding', 'content-type', 'content-length'): - continue + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == '\r\n': + # Normal end of headers + break + + if line[0] in ' \t': + # It's a continuation line. + v = line.strip() + else: + k, v = line.split(":", 1) + k, v = k.strip().upper(), v.strip() + envname = "HTTP_" + k.replace("-", "_") - envname = "HTTP_" + k.upper().replace("-", "_") if k in comma_separated_headers: existing = environ.get(envname) if existing: - environ[envname] = ", ".join([existing] + headers.getheaders(k)) - else: - environ[envname] = ", ".join(headers.getheaders(k)) - else: - environ[envname] = headers[k] - return environ + v = ", ".join((existing, v)) + environ[envname] = v + + ct = environ.pop("HTTP_CONTENT_TYPE", None) + if ct: + environ["CONTENT_TYPE"] = ct + cl = environ.pop("HTTP_CONTENT_LENGTH", None) + if cl: + environ["CONTENT_LENGTH"] = cl def decode_chunked(self): """Decode the 'chunked' transfer coding.""" @@ -268,29 +350,20 @@ class HTTPRequest(object): "(expected '\\r\\n', got %r)" % crlf) return - headers = mimetools.Message(self.rfile) - self.environ.update(self.parse_headers(headers)) + # Grab any trailer headers + self.read_headers() + data.seek(0) self.environ["wsgi.input"] = data self.environ["CONTENT_LENGTH"] = str(cl) or "" return True def respond(self): - wfile = self.connection.wfile + """Call the appropriate WSGI app and write its iterable output.""" response = self.wsgi_app(self.environ, self.start_response) try: - for line in response: - if not self.sent_headers: - self.sent_headers = True - self.send_headers() - if self.chunked_write: - wfile.write(hex(len(line))[2:]) - wfile.write("\r\n") - wfile.write(line) - wfile.write("\r\n") - else: - wfile.write(line) - wfile.flush() + for chunk in response: + self.write(chunk) finally: if hasattr(response, "close"): response.close() @@ -299,27 +372,26 @@ class HTTPRequest(object): self.sent_headers = True self.send_headers() if self.chunked_write: - wfile.write("0\r\n\r\n") - wfile.flush() + self.sendall("0\r\n\r\n") def simple_response(self, status, msg=""): """Write a simple response back to the client.""" status = str(status) - wfile = self.connection.wfile - wfile.write("%s %s\r\n" % (self.connection.server.protocol, status)) - wfile.write("Content-Length: %s\r\n" % len(msg)) + buf = ["%s %s\r\n" % (self.connection.server.protocol, status), + "Content-Length: %s\r\n" % len(msg)] if status[:3] == "413" and self.response_protocol == 'HTTP/1.1': # Request Entity Too Large self.close_connection = True - wfile.write("Connection: close\r\n") + buf.append("Connection: close\r\n") - wfile.write("\r\n") + buf.append("\r\n") if msg: - wfile.write(msg) - wfile.flush() + buf.append(msg) + self.sendall("".join(buf)) def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" if self.started_response: if not exc_info: assert False, "Already started response" @@ -333,49 +405,64 @@ class HTTPRequest(object): self.outheaders.extend(headers) return self.write - def write(self, d): + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ if not self.sent_headers: self.sent_headers = True self.send_headers() - self.connection.wfile.write(d) - self.connection.wfile.flush() + if self.chunked_write: + buf = [hex(len(chunk))[2:], + "\r\n", chunk, "\r\n"] + self.sendall("".join(buf)) + else: + self.sendall(chunk) def send_headers(self): - hkeys = [key.lower() for (key, value) in self.outheaders] + """Assert, process, and send the HTTP response message-headers.""" + hkeys = [key.lower() for key, value in self.outheaders] status = int(self.status[:3]) - if self.response_protocol == 'HTTP/1.1': - if status == 413: - # Request Entity Too Large. Close conn to avoid garbage. - self.close_connection = True - elif "content-length" not in hkeys: - if status in (200, 203, 206): + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if self.response_protocol == 'HTTP/1.1': # Use the chunked transfer-coding self.chunked_write = True self.outheaders.append(("Transfer-Encoding", "chunked")) - # "All 1xx (informational), 204 (no content), - # and 304 (not modified) responses MUST NOT - # include a message-body." - elif status >= 200 and status not in (204, 205, 304): - # Close conn to determine transfer-length. + else: + # Closing the conn is the only way to determine len. self.close_connection = True - if self.close_connection and "connection" not in hkeys: - self.outheaders.append(("Connection", "close")) + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) if "date" not in hkeys: self.outheaders.append(("Date", rfc822.formatdate())) server = self.connection.server - wfile = self.connection.wfile if "server" not in hkeys: self.outheaders.append(("Server", server.version)) - wfile.write(server.protocol + " " + self.status + "\r\n") + buf = [server.protocol, " ", self.status, "\r\n"] try: - for k, v in self.outheaders: - wfile.write(k + ": " + v + "\r\n") + buf += [k + ": " + v + "\r\n" for k, v in self.outheaders] except TypeError: if not isinstance(k, str): raise TypeError("WSGI response header key %r is not a string.") @@ -383,11 +470,16 @@ class HTTPRequest(object): raise TypeError("WSGI response header value %r is not a string.") else: raise - wfile.write("\r\n") - wfile.flush() + buf.append("\r\n") + self.sendall("".join(buf)) -def _ssl_wrap_method(method): +def _ssl_wrap_method(method, is_reader=False): + """Wrap the given method with SSL error-trapping. + + is_reader: if False (the default), EOF errors will be raised. + If True, EOF errors will return "" (to emulate normal sockets). + """ def ssl_method_wrapper(self, *args, **kwargs): ## print (id(self), method, args, kwargs) start = time.time() @@ -395,24 +487,25 @@ def _ssl_wrap_method(method): try: return method(self, *args, **kwargs) except (SSL.WantReadError, SSL.WantWriteError): - # Sleep and try again + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. time.sleep(self.ssl_retry) except SSL.SysCallError, e: - if e.args == (-1, 'Unexpected EOF'): + if is_reader and e.args == (-1, 'Unexpected EOF'): return "" errno = e.args[0] - if errno not in socket_errors_to_ignore: - raise socket.error(errno) - - return "" + if is_reader and errno in socket_errors_to_ignore: + return "" + raise socket.error(errno) except SSL.Error, e: - if e.args == (-1, 'Unexpected EOF'): + if is_reader and e.args == (-1, 'Unexpected EOF'): return "" - elif e.args[0][0][2] == 'ssl handshake failure': + if is_reader and e.args[0][0][2] == 'ssl handshake failure': return "" - else: - raise + raise if time.time() - start > self.ssl_timeout: raise socket.timeout("timed out") return ssl_method_wrapper @@ -427,15 +520,28 @@ class SSL_fileobject(socket._fileobject): flush = _ssl_wrap_method(socket._fileobject.flush) write = _ssl_wrap_method(socket._fileobject.write) writelines = _ssl_wrap_method(socket._fileobject.writelines) - read = _ssl_wrap_method(socket._fileobject.read) - readline = _ssl_wrap_method(socket._fileobject.readline) - readlines = _ssl_wrap_method(socket._fileobject.readlines) + read = _ssl_wrap_method(socket._fileobject.read, is_reader=True) + readline = _ssl_wrap_method(socket._fileobject.readline, is_reader=True) + readlines = _ssl_wrap_method(socket._fileobject.readlines, is_reader=True) class HTTPConnection(object): + """An HTTP connection (active socket). + + socket: the raw socket object (usually TCP) for this connection. + addr: the "bind address" for the remote end of the socket. + For IP sockets, this is a tuple of (REMOTE_ADDR, REMOTE_PORT). + For UNIX domain sockets, this will be a string. + server: the HTTP Server for this Connection. Usually, the server + object possesses a passive (server) socket which spawns multiple, + active (client) sockets, one for each connection. + + environ: a WSGI environ template. This will be copied for each request. + rfile: a fileobject for reading from the socket. + sendall: a function for writing (+ flush) to the socket. + """ rbufsize = -1 - wbufsize = -1 RequestHandlerClass = HTTPRequest environ = {"wsgi.version": (1, 0), "wsgi.url_scheme": "http", @@ -454,16 +560,18 @@ class HTTPConnection(object): self.environ = self.environ.copy() if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() self.rfile = SSL_fileobject(sock, "r", self.rbufsize) - self.wfile = SSL_fileobject(sock, "w", self.wbufsize) + self.rfile.ssl_timeout = timeout + self.sendall = _ssl_wrap_method(sock.sendall) self.environ["wsgi.url_scheme"] = "https" self.environ["HTTPS"] = "on" sslenv = getattr(server, "ssl_environ", None) if sslenv: self.environ.update(sslenv) else: - self.rfile = self.socket.makefile("r", self.rbufsize) - self.wfile = self.socket.makefile("w", self.wbufsize) + self.rfile = sock.makefile("r", self.rbufsize) + self.sendall = sock.sendall self.environ.update({"wsgi.input": self.rfile, "SERVER_NAME": self.server.server_name, @@ -510,8 +618,8 @@ class HTTPConnection(object): req.simple_response("500 Internal Server Error", format_exc()) def close(self): + """Close the socket underlying this connection.""" self.rfile.close() - self.wfile.close() self.socket.close() @@ -527,6 +635,18 @@ def format_exc(limit=None): _SHUTDOWNREQUEST = None class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + server: the HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it. + ready: a simple flag for the calling server to know when this thread + has begun polling the Queue. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ def __init__(self, server): self.ready = False @@ -550,6 +670,11 @@ class WorkerThread(threading.Thread): class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + *args: the arguments to create the wrapped SSL.Connection(*args). + """ + def __init__(self, *args): self._ssl_conn = SSL.Connection(*args) self._lock = threading.RLock() @@ -585,10 +710,27 @@ class CherryPyWSGIServer(object): request_queue_size: the 'backlog' argument to socket.listen(); specifies the maximum number of queued connections (default 5). timeout: the timeout in seconds for accepted connections (default 10). + + protocol: the version string to write in the Status-Line of all + HTTP responses. For example, "HTTP/1.1" (the default). This + also limits the supported features used in the response. + + + SSL/HTTPS + --------- + The OpenSSL module must be importable for SSL functionality. + You can obtain it from http://pyopenssl.sourceforge.net/ + + ssl_certificate: the filename of the server SSL certificate. + ssl_privatekey: the filename of the server's private key file. + + If either of these is None (both are None by default), this server + will not use SSL. If both are given and are valid, they will be read + on server start and used in the SSL context for the listening socket. """ protocol = "HTTP/1.1" - version = "CherryPy/3.0.0RC1" + version = "CherryPy/3.0.0" ready = False _interrupt = None ConnectionClass = HTTPConnection @@ -631,22 +773,6 @@ class CherryPyWSGIServer(object): # trap those exceptions in whatever code block calls start(). self._interrupt = None - def bind(family, type, proto=0): - """Create (or recreate) the actual socket object.""" - self.socket = socket.socket(family, type, proto) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if self.ssl_certificate and self.ssl_private_key: - if SSL is None: - raise ImportError("You must install pyOpenSSL to use HTTPS.") - - # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.use_privatekey_file(self.ssl_private_key) - ctx.use_certificate_file(self.ssl_certificate) - self.socket = SSLConnection(ctx, self.socket) - self.populate_ssl_environ() - self.socket.bind(self.bind_addr) - # Select the appropriate socket if isinstance(self.bind_addr, basestring): # AF_UNIX socket @@ -664,9 +790,21 @@ class CherryPyWSGIServer(object): # AF_INET or AF_INET6 socket # Get the correct address family for our host (allows IPv6 addresses) host, port = self.bind_addr + flags = 0 + if host == '': + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + host = None + flags = socket.AI_PASSIVE try: info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM) + socket.SOCK_STREAM, 0, flags) except socket.gaierror: # Probably a DNS issue. Assume IPv4. info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", self.bind_addr)] @@ -676,7 +814,7 @@ class CherryPyWSGIServer(object): for res in info: af, socktype, proto, canonname, sa = res try: - bind(af, socktype, proto) + self.bind(af, socktype, proto) except socket.error, msg: if self.socket: self.socket.close() @@ -709,7 +847,25 @@ class CherryPyWSGIServer(object): time.sleep(0.1) raise self.interrupt + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +## self.socket.setsockopt(socket.SOL_SOCKET, socket.TCP_NODELAY, 1) + if self.ssl_certificate and self.ssl_private_key: + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(self.ssl_private_key) + ctx.use_certificate_file(self.ssl_certificate) + self.socket = SSLConnection(ctx, self.socket) + self.populate_ssl_environ() + self.socket.bind(self.bind_addr) + def tick(self): + """Accept a new connection and put it on the Queue.""" try: s, addr = self.socket.accept() if not self.ready: @@ -739,7 +895,9 @@ class CherryPyWSGIServer(object): self._interrupt = True self.stop() self._interrupt = interrupt - interrupt = property(_get_interrupt, _set_interrupt) + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") def stop(self): """Gracefully shutdown a server that is serving forever.""" @@ -755,6 +913,10 @@ class CherryPyWSGIServer(object): if x.args[1] != "Bad file descriptor": raise else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it would if we bound to INADDR_ANY via host = ''. for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res |