Robert Brewer avatar Robert Brewer committed b27bee1

Copied recent changes from trunk to _cpwsgiserver3.

Comments (0)

Files changed (1)

cherrypy/_cpwsgiserver3.py

-"""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
 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
         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").
             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)
             return
         
         if scheme:
-            self.environ["wsgi.url_scheme"] = scheme
+            environ["wsgi.url_scheme"] = scheme
         if params:
             path = path + ";" + params
         
         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:
         
         # 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
             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
         #
         # 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.")
             
-            envname = "HTTP_" + k.upper().replace("-", "_")
+            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("-", "_")
+            
             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."""
                                      "(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()
             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"
         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.")
                 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()
             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
     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",
         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,
                 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()
 
 
 _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
 
 
 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()
     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
         # 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
             # 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)]
         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()
                     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:
         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."""
                     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
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.