diff --git a/CHANGES.rst b/CHANGES.rst index e88b89e740..59331cc7fd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,9 @@ dev (master) * ... [Short description of non-trivial change.] (Issue #) +* Add a server_hostname parameter to HTTPSConnection which allows for + overriding the SNI hostname sent in the handshake. (Pull #1397) + 1.23 (2018-06-05) ----------------- diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py index 1813c4d46c..f6a3a43ccb 100644 --- a/test/with_dummyserver/test_https.py +++ b/test/with_dummyserver/test_https.py @@ -323,6 +323,23 @@ def test_assert_specific_hostname(self): https_pool.assert_hostname = 'localhost' https_pool.request('GET', '/') + def test_server_hostname(self): + https_pool = HTTPSConnectionPool('127.0.0.1', self.port, + cert_reqs='CERT_REQUIRED', + ca_certs=DEFAULT_CA, + server_hostname='localhost') + self.addCleanup(https_pool.close) + + conn = https_pool._new_conn() + conn.request('GET', '/') + + # Assert the wrapping socket is using the passed-through SNI name. + # pyopenssl doesn't let you pull the server_hostname back off the + # socket, so only add this assertion if the attribute is there (i.e. + # the python ssl module). + if hasattr(conn.sock, 'server_hostname'): + self.assertEqual(conn.sock.server_hostname, "localhost") + def test_assert_fingerprint_md5(self): https_pool = HTTPSConnectionPool('localhost', self.port, cert_reqs='CERT_REQUIRED', diff --git a/urllib3/connection.py b/urllib3/connection.py index a03b573f01..4f662b0d68 100644 --- a/urllib3/connection.py +++ b/urllib3/connection.py @@ -242,7 +242,7 @@ class HTTPSConnection(HTTPConnection): def __init__(self, host, port=None, key_file=None, cert_file=None, strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - ssl_context=None, **kw): + ssl_context=None, server_hostname=None, **kw): HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw) @@ -250,6 +250,7 @@ def __init__(self, host, port=None, key_file=None, cert_file=None, self.key_file = key_file self.cert_file = cert_file self.ssl_context = ssl_context + self.server_hostname = server_hostname # Required property for Google AppEngine 1.9.0 which otherwise causes # HTTPS requests to go out as HTTP. (See Issue #356) @@ -270,6 +271,7 @@ def connect(self): keyfile=self.key_file, certfile=self.cert_file, ssl_context=self.ssl_context, + server_hostname=self.server_hostname ) @@ -328,6 +330,10 @@ def connect(self): # Override the host with the one we're requesting data from. hostname = self._tunnel_host + server_hostname = hostname + if self.server_hostname is not None: + server_hostname = self.server_hostname + is_time_off = datetime.date.today() < RECENT_DATE if is_time_off: warnings.warn(( @@ -352,7 +358,7 @@ def connect(self): certfile=self.cert_file, ca_certs=self.ca_certs, ca_cert_dir=self.ca_cert_dir, - server_hostname=hostname, + server_hostname=server_hostname, ssl_context=context) if self.assert_fingerprint: @@ -373,7 +379,7 @@ def connect(self): 'for details.)'.format(hostname)), SubjectAltNameWarning ) - _match_hostname(cert, self.assert_hostname or hostname) + _match_hostname(cert, self.assert_hostname or server_hostname) self.is_verified = ( context.verify_mode == ssl.CERT_REQUIRED or