# 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 = {
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:
# 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()
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:
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:
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):
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!"))