]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Client-side ipv6 support (disabled by default)
authorBen Darnell <ben@bendarnell.com>
Mon, 16 May 2011 03:01:58 +0000 (20:01 -0700)
committerBen Darnell <ben@bendarnell.com>
Mon, 16 May 2011 03:01:58 +0000 (20:01 -0700)
tornado/curl_httpclient.py
tornado/httpclient.py
tornado/simple_httpclient.py
tornado/test/simple_httpclient_test.py

index fea02fb3b5c5499ff7908b662d46a274409cb65e..e911596a616ce92a21effca95d16d6b4857816a6 100644 (file)
@@ -353,6 +353,12 @@ def _curl_setup_request(curl, request, buffer, headers):
         # request uses a custom ca_certs file, they all must.
         pass
 
+    if request.allow_ipv6 is False:
+        # Curl behaves reasonably when DNS resolution gives an ipv6 address
+        # that we can't reach, so allow ipv6 unless the user asks to disable.
+        # (but see version check in _process_queue above)
+        curl.setopt(pycurl.IPRESOLVE, pycurl.IPRESOLVE_V4)
+
     # Set the request method through curl's retarded interface which makes
     # up names for almost every single method
     curl_options = {
index 5c462e871f17803863fc09407b0fcbd1623b40d9..af77f777dd3d7a442d428dfa03b161033ae93c19 100644 (file)
@@ -154,7 +154,8 @@ class HTTPRequest(object):
                  header_callback=None, prepare_curl_callback=None,
                  proxy_host=None, proxy_port=None, proxy_username=None,
                  proxy_password='', allow_nonstandard_methods=False,
-                 validate_cert=True, ca_certs=None):
+                 validate_cert=True, ca_certs=None,
+                 allow_ipv6=None):
         if headers is None:
             headers = httputil.HTTPHeaders()
         if if_modified_since:
@@ -195,6 +196,9 @@ class HTTPRequest(object):
         # SimpleAsyncHTTPClient does not have this limitation.
         self.validate_cert = validate_cert
         self.ca_certs = ca_certs
+        # allow_ipv6 may be True, False, or None for default behavior
+        # that varies by httpclient implementation.
+        self.allow_ipv6 = allow_ipv6
         self.start_time = time.time()
 
 
index bc3c21203b5b052c20a5e9c93a2a694e63074d2f..030d2640ade0d5881217fc2db665f9537ca53372 100644 (file)
@@ -134,14 +134,35 @@ class _HTTPConnection(object):
         self._timeout = None
         with stack_context.StackContext(self.cleanup):
             parsed = urlparse.urlsplit(_unicode(self.request.url))
-            host = parsed.hostname
-            if parsed.port is None:
-                port = 443 if parsed.scheme == "https" else 80
+            # urlsplit results have hostname and port results, but they
+            # didn't support ipv6 literals until python 2.7.
+            netloc = parsed.netloc
+            if "@" in netloc:
+                userpass, _, netloc = netloc.rpartition("@")
+            match = re.match(r'^(.+):(\d+)$', netloc)
+            if match:
+                host = match.group(1)
+                port = int(match.group(2))
             else:
-                port = parsed.port
+                host = netloc
+                port = 443 if parsed.scheme == "https" else 80
+            if re.match(r'^\[.*\]$', host):
+                # raw ipv6 addresses in urls are enclosed in brackets
+                host = host[1:-1]
             if self.client.hostname_mapping is not None:
                 host = self.client.hostname_mapping.get(host, host)
 
+            if request.allow_ipv6:
+                af = socket.AF_UNSPEC
+            else:
+                # We only try the first IP we get from getaddrinfo,
+                # so restrict to ipv4 by default.
+                af = socket.AF_INET
+
+            addrinfo = socket.getaddrinfo(host, port, af, socket.SOCK_STREAM,
+                                          0, 0)
+            af, socktype, proto, canonname, sockaddr = addrinfo[0]
+
             if parsed.scheme == "https":
                 ssl_options = {}
                 if request.validate_cert:
@@ -150,11 +171,11 @@ class _HTTPConnection(object):
                     ssl_options["ca_certs"] = request.ca_certs
                 else:
                     ssl_options["ca_certs"] = _DEFAULT_CA_CERTS
-                self.stream = SSLIOStream(socket.socket(),
+                self.stream = SSLIOStream(socket.socket(af, socktype, proto),
                                           io_loop=self.io_loop,
                                           ssl_options=ssl_options)
             else:
-                self.stream = IOStream(socket.socket(),
+                self.stream = IOStream(socket.socket(af, socktype, proto),
                                        io_loop=self.io_loop)
             timeout = min(request.connect_timeout, request.request_timeout)
             if timeout:
@@ -162,7 +183,7 @@ class _HTTPConnection(object):
                     self.start_time + timeout,
                     self._on_timeout)
             self.stream.set_close_callback(self._on_close)
-            self.stream.connect((host, port),
+            self.stream.connect(sockaddr,
                                 functools.partial(self._on_connect, parsed))
 
     def _on_timeout(self):
index 2b82b36b80d9aef9e64751bc8f09e127c0ceb8aa..0478e1a1655f7168134eb6a949837e1f9c84a6f1 100644 (file)
@@ -257,3 +257,15 @@ class SimpleHTTPClientTestCase(AsyncHTTPTestCase, LogTrapTestCase):
                               user_agent=u"foo")
         self.assertEqual(response.headers["Content-Length"], "1")
         self.assertEqual(response.body, byte_body)
+
+    def test_ipv6(self):
+        url = self.get_url("/hello").replace("localhost", "[::1]")
+
+        # ipv6 is currently disabled by default and must be explicitly requested
+        self.http_client.fetch(url, self.stop)
+        response = self.wait()
+        self.assertEqual(response.code, 599)
+
+        self.http_client.fetch(url, self.stop, allow_ipv6=True)
+        response = self.wait()
+        self.assertEqual(response.body, b("Hello world!"))