Commits

Robert Brewer committed 5a5d9c7

Changes to socket_host:

1. wsgiserver now treats a host of "" as an alias for INADDR_ANY. The getaddrinfo call now passes host=None and sets AI_PASSIVE in this case.
2. Server.httpserver_from_self doesn't change an empty host ("") to localhost anymore.
3. The test suite has a new {{{--host=<name or IP>}}} flag.
4. The webtest module now allows WebCase.HOST to be "", and will connect on '127.0.0.1' if so.
5. Lots of comments throughout to explain that the server's treatment of {{{socket_host=""}}} (all IPs) is different than the client's (pick any IP).
6. Hopefully, this will fix #619 (long delay on startup).

Comments (0)

Files changed (9)

cherrypy/__init__.py

             else:
                 host = server.socket_host
                 if not host:
+                    # The empty string signifies INADDR_ANY.
+                    # Look up the host name, which should be
+                    # the safest thing to spit out in a URL.
                     import socket
                     host = socket.gethostname()
                 port = server.socket_port

cherrypy/_cpserver.py

         
         host = self.socket_host
         port = self.socket_port
-        if not host:
-            host = 'localhost'
         return httpserver, (host, port)
     
     def start(self):
 def check_port(host, port):
     """Raise an error if the given port is not free on the given host."""
     if not host:
+        # The empty string signifies INADDR_ANY,
+        # which should respond on localhost.
         host = 'localhost'
     port = int(port)
     
 def wait_for_free_port(host, port):
     """Wait for the specified port to become free (drop requests)."""
     if not host:
+        # The empty string signifies INADDR_ANY,
+        # which should respond on localhost.
         host = 'localhost'
     
     for trial in xrange(50):
 def wait_for_occupied_port(host, port):
     """Wait for the specified port to become active (receive requests)."""
     if not host:
+        # The empty string signifies INADDR_ANY,
+        # which should respond on localhost.
         host = 'localhost'
     
     for trial in xrange(50):

cherrypy/test/helper.py

             port = ""
         else:
             port = ":%s" % self.PORT
-        return "%s://%s%s%s" % (self.scheme, self.HOST, port,
+        
+        host = self.HOST
+        if not host:
+            # The empty string signifies INADDR_ANY,
+            # which should respond on localhost.
+            host = "127.0.0.1"
+        
+        return "%s://%s%s%s" % (self.scheme, host, port,
                                 self.script_name.rstrip("/"))
     
     def exit(self):

cherrypy/test/test.py

     """A test harness for the CherryPy framework and CherryPy applications."""
     
     def __init__(self, tests=None, server=None, protocol="HTTP/1.1",
-                 port=8000, scheme="http", interactive=True):
+                 port=8000, scheme="http", interactive=True, host='127.0.0.1'):
         """Constructor to populate the TestHarness instance.
         
         tests should be a list of module names (strings).
         self.server = server
         self.protocol = protocol
         self.port = port
+        self.host = host
         self.scheme = scheme
         self.interactive = interactive
     
         
         if isinstance(conf, basestring):
             conf = cherrypy.config._Parser().dict_from_file(conf)
-        baseconf = {'server.socket_host': '127.0.0.1',
+        baseconf = {'server.socket_host': self.host,
                     'server.socket_port': self.port,
                     'server.thread_pool': 10,
                     'environment': "test_suite",
         # and we wouldn't be able to globally override the port anymore.
         from cherrypy.test import helper, webtest
         webtest.WebCase.PORT = self.port
+        webtest.WebCase.HOST = self.host
         webtest.WebCase.harness = self
         helper.CPWebCase.scheme = self.scheme
         webtest.WebCase.interactive = self.interactive
     scheme = "http"
     protocol = "HTTP/1.1"
     port = 8080
+    host = '127.0.0.1'
     cover = False
     profile = False
     validate = False
         
         longopts = ['cover', 'profile', 'validate', 'conquer', 'dumb',
                     '1.0', 'ssl', 'help',
-                    'basedir=', 'port=', 'server=']
+                    'basedir=', 'port=', 'server=', 'host=']
         longopts.extend(self.available_tests)
         try:
             opts, args = getopt.getopt(args, "", longopts)
                 self.basedir = a
             elif o == "--port":
                 self.port = int(a)
+            elif o == "--host":
+                self.host = a
             elif o == "--server":
                 if a in self.available_servers:
                     a = self.available_servers[a]
         
         print """CherryPy Test Program
     Usage:
-        test.py --server=* --port=%s --1.0 --cover --basedir=path --profile --validate --conquer --dumb --tests**
+        test.py --server=* --host=%s --port=%s --1.0 --cover --basedir=path --profile --validate --conquer --dumb --tests**
         
-    """ % self.__class__.port
+    """ % (self.__class__.host, self.__class__.port)
         print '    * servers:'
         for name, val in self.available_servers.iteritems():
             if name == self.default_server:
         
         print """
     
+    --host=<name or IP addr>: use a host other than the default (%s).
+        Not yet available with mod_python servers.
     --port=<int>: use a port other than the default (%s)
     --1.0: use HTTP/1.0 servers instead of default HTTP/1.1
     
     --validate: use wsgiref.validate (builtin in Python 2.5).
     --conquer: use wsgiconq (which uses pyconquer) to trace calls.
     --dumb: turn off the interactive output features.
-    """ % self.__class__.port
+    """ % (self.__class__.host, self.__class__.port)
         
         print '    ** tests:'
         for name in self.available_tests:
             h.use_wsgi = True
         else:
             h = TestHarness(self.tests, self.server, self.protocol,
-                            self.port, self.scheme, self.interactive)
+                            self.port, self.scheme, self.interactive,
+                            self.host)
         
         success = h.run(conf)
         

cherrypy/test/test_core.py

             hMap['content-type'] = "text/html"
             hMap['content-length'] = 18
             hMap['server'] = 'CherryPy headertest'
-            hMap['location'] = ('%s://127.0.0.1:%s/headers/'
-                                % (cherrypy.request.remote.port,
+            hMap['location'] = ('%s://%s:%s/headers/'
+                                % (cherrypy.request.local.ip,
+                                   cherrypy.request.local.port,
                                    cherrypy.request.scheme))
             
             # Set a rare header for fun
         self.assertStatus(200)
         
         data = open(log_access_file, "rb").readlines()
-        self.assertEqual(data[0][:15], '127.0.0.1 - - [')
+        
+        host = self.HOST
+        if not host:
+            # The empty string signifies INADDR_ANY,
+            # which should respond on localhost.
+            host = "127.0.0.1"
+        intro = '%s - - [' % host
+        
+        if not data[0].startswith(intro):
+            self.fail("%r doesn't start with %r" % (data[0], intro))
         haslength = False
         for k, v in self.headers:
             if k.lower() == 'content-length':
                                  % self.prefix()):
                 self.fail(line)
         
-        self.assertEqual(data[-1][:15], '127.0.0.1 - - [')
+        if not data[-1].startswith(intro):
+            self.fail("%r doesn't start with %r" % (data[-1], intro))
         haslength = False
         for k, v in self.headers:
             if k.lower() == 'content-length':
         # This hangs in rev 891 and earlier.
         lines256 = "x" * 248
         self.getPage("/",
-                     headers=[('Host', '127.0.0.1:%s' % self.PORT),
+                     headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
                               ('From', lines256)])
         
         # Test upload

cherrypy/test/test_proxy.py

                 port = ":%s" % self.PORT
             elif self.scheme == "https" and self.PORT != 443:
                 port = ":%s" % self.PORT
+            host = self.HOST
+            if host == '':
+                import socket
+                host = socket.gethostname()
             self.assertEqual(cherrypy.url("/this/new/page", script_name=sn),
                              "%s://%s%s%s/this/new/page"
-                             % (self.scheme, self.HOST, port, sn))
+                             % (self.scheme, host, port, sn))
         
         # Test trailing slash (see http://www.cherrypy.org/ticket/562).
         self.getPage("/xhost/", headers=[('X-Host', 'www.yetanother.com')])

cherrypy/test/test_xmlrpc.py

         
         # load the appropriate xmlrpc proxy
         if getattr(self.harness, "scheme", "http") == "https":
-            url = 'https://localhost:%s/xmlrpc/' % self.PORT
+            url = 'https://%s:%s/xmlrpc/' % (self.HOST, self.PORT)
             proxy = xmlrpclib.ServerProxy(url, transport=HTTPSTransport())
         else:
-            url = 'http://localhost:%s/xmlrpc/' % self.PORT
+            url = 'http://%s:%s/xmlrpc/' % (self.HOST, self.PORT)
             proxy = xmlrpclib.ServerProxy(url)
         
         # begin the tests ...

cherrypy/test/webtest.py

             cls = httplib.HTTPConnection
         
         if on:
-            self.HTTP_CONN = cls(self.HOST, self.PORT)
+            host = self.HOST
+            if not host:
+                # The empty string signifies INADDR_ANY,
+                # which should respond on localhost.
+                host = "127.0.0.1"
+            self.HTTP_CONN = cls(host, self.PORT)
             # Automatically re-connect?
             self.HTTP_CONN.auto_open = auto_open
             self.HTTP_CONN.connect()
         ServerError.on = False
         
         self.url = url
-        result = openURL(url, headers, method, body, self.HOST, self.PORT,
+        host = self.HOST
+        if not host:
+            # The empty string signifies INADDR_ANY,
+            # which should respond on localhost.
+            host = "127.0.0.1"
+        result = openURL(url, headers, method, body, host, self.PORT,
                          self.HTTP_CONN, protocol or self.PROTOCOL)
         self.status, self.headers, self.body = result
         

cherrypy/wsgiserver.py

             # 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.
+                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)]
                     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