Use FQDN only for DNS and drop trailing dot for other operations
MODIFY
dummyserver/testcase.py
dummyserver/testcase.py
@@ -76,6 +76,23 @@ class SocketDummyServerTestCase(unittest.TestCase):
if hasattr(cls, 'server_thread'):
cls.server_thread.join(0.1)
+ def assert_header_received(
+ self,
+ received_headers,
+ header_name,
+ expected_value=None
+ ):
+ header_name = header_name.encode('ascii')
+ if expected_value is not None:
+ expected_value = expected_value.encode('ascii')
+ header_titles = []
+ for header in received_headers:
+ key, value = header.split(b': ')
+ header_titles.append(key)
+ if key == header_name and expected_value is not None:
+ self.assertEqual(value, expected_value)
+ self.assertIn(header_name, header_titles)
+
class IPV4SocketDummyServerTestCase(SocketDummyServerTestCase):
@classmethod

MODIFY
test/test_connectionpool.py
test/test_connectionpool.py
@@ -118,6 +118,7 @@ class TestConnectionPool(object):
('google.com', 'https://google.com/'),
('yahoo.com', 'http://google.com/'),
('google.com', 'https://google.net/'),
+ ('google.com', 'http://google.com./'),
])
def test_not_same_host_no_port_http(self, a, b):
with HTTPConnectionPool(a) as c:
@@ -130,6 +131,7 @@ class TestConnectionPool(object):
('google.com', 'http://google.com/'),
('yahoo.com', 'https://google.com/'),
('google.com', 'https://google.net/'),
+ ('google.com', 'https://google.com./'),
])
def test_not_same_host_no_port_https(self, a, b):
with HTTPSConnectionPool(a) as c:

MODIFY
test/test_poolmanager.py
test/test_poolmanager.py
@@ -31,6 +31,14 @@ class TestPoolManager(object):
assert conn1 == conn2
+ # Ensure that FQDNs are handled separately from relative domains
+ p = PoolManager(2)
+
+ conn1 = p.connection_from_url('http://localhost.:8081/foo')
+ conn2 = p.connection_from_url('http://localhost:8081/bar')
+
+ assert conn1 != conn2
+
def test_many_urls(self):
urls = [
"http://localhost:8081/foo",

MODIFY
test/with_dummyserver/test_https.py
test/with_dummyserver/test_https.py
@@ -65,6 +65,11 @@ class TestHTTPS(HTTPSDummyServerTestCase):
r = self._pool.request('GET', '/')
self.assertEqual(r.status, 200, r.data)
+ def test_dotted_fqdn(self):
+ pool = HTTPSConnectionPool(self.host + '.', self.port)
+ r = pool.request('GET', '/')
+ self.assertEqual(r.status, 200, r.data)
+
def test_set_ssl_version_to_tlsv1(self):
self._pool.ssl_version = ssl.PROTOCOL_TLSv1
r = self._pool.request('GET', '/')

MODIFY
test/with_dummyserver/test_socketlevel.py
test/with_dummyserver/test_socketlevel.py
@@ -1225,6 +1225,37 @@ class TestHeaders(SocketDummyServerTestCase):
pool.request('GET', '/', headers=OrderedDict(expected_request_headers))
self.assertEqual(expected_request_headers, actual_request_headers)
+ def test_request_host_header_ignores_fqdn_dot(self):
+
+ received_headers = []
+
+ def socket_handler(listener):
+ sock = listener.accept()[0]
+
+ buf = b''
+ while not buf.endswith(b'\r\n\r\n'):
+ buf += sock.recv(65536)
+
+ for header in buf.split(b'\r\n')[1:]:
+ if header:
+ received_headers.append(header)
+
+ sock.send((
+ u'HTTP/1.1 204 No Content\r\n'
+ u'Content-Length: 0\r\n'
+ u'\r\n').encode('ascii'))
+
+ sock.close()
+
+ self._start_server(socket_handler)
+
+ pool = HTTPConnectionPool(self.host + '.', self.port, retries=False)
+ self.addCleanup(pool.close)
+ pool.request('GET', '/')
+ self.assert_header_received(
+ received_headers, 'Host', '%s:%s' % (self.host, self.port)
+ )
+
def test_response_headers_are_returned_in_the_original_order(self):
# NOTE: Probability this test gives a false negative is 1/(K!)
K = 16

MODIFY
urllib3/connection.py
urllib3/connection.py
@@ -124,6 +124,35 @@ class HTTPConnection(_HTTPConnection, object):
# Superclass also sets self.source_address in Python 2.7+.
_HTTPConnection.__init__(self, *args, **kw)
+ @property
+ def host(self):
+ """
+ Getter method to remove any trailing dots that indicate the hostname is an FQDN.
+
+ In general, SSL certificates don't include the trailing dot indicating a
+ fully-qualified domain name, and thus, they don't validate properly when
+ checked against a domain name that includes the dot. In addition, some
+ servers may not expect to receive the trailing dot when provided.
+
+ However, the hostname with trailing dot is critical to DNS resolution; doing a
+ lookup with the trailing dot will properly only resolve the appropriate FQDN,
+ whereas a lookup without a trailing dot will search the system's search domain
+ list. Thus, it's important to keep the original host around for use only in
+ those cases where it's appropriate (i.e., when doing DNS lookup to establish the
+ actual TCP connection across which we're going to send HTTP requests).
+ """
+ return self._dns_host.rstrip('.')
+
+ @host.setter
+ def host(self, value):
+ """
+ Setter for the `host` property.
+
+ We assume that only urllib3 uses the _dns_host attribute; httplib itself
+ only uses `host`, and it seems reasonable that other libraries follow suit.
+ """
+ self._dns_host = value
+
def _new_conn(self):
""" Establish a socket connection and set nodelay settings on it.
@@ -138,7 +167,7 @@ class HTTPConnection(_HTTPConnection, object):
try:
conn = connection.create_connection(
- (self.host, self.port), self.timeout, **extra_kw)
+ (self._dns_host, self.port), self.timeout, **extra_kw)
except SocketTimeout as e:
raise ConnectTimeoutError(

Use FQDN only for DNS and drop trailing dot for other operations