summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Brewer <fumanchu@aminus.org>2006-12-28 20:10:07 +0000
committerRobert Brewer <fumanchu@aminus.org>2006-12-28 20:10:07 +0000
commite0078b45167db6f6927011140753bebe602b157d (patch)
tree8e11ebc2a6f0280e8804030345015b92619182fd
parent66befe917c1158ce2a4a48f5372cba52535ffe96 (diff)
downloadcherrypy-e0078b45167db6f6927011140753bebe602b157d.tar.gz
Copied recent changes from trunk to _cpwsgiserver3.
-rw-r--r--cherrypy/_cpwsgiserver3.py456
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