From 5325a46c8a5a722a552926c5210eb20181cb87e8 Mon Sep 17 00:00:00 2001 From: Ian Haken Date: Wed, 11 Jul 2018 21:05:34 -0700 Subject: [PATCH] Support a servername parameter on HTTPSConnections which overrides the name used for SNI/hostname verification. (#1397) --- CHANGES.rst | 3 +++ src/urllib3/connection.py | 12 +++++++++--- test/with_dummyserver/test_https.py | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3ae3b7f844..683001c0e6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,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/src/urllib3/connection.py b/src/urllib3/connection.py index a03b573f01..4f662b0d68 100644 --- a/src/urllib3/connection.py +++ b/src/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 diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py index 36910243de..8c9cd16c8a 100644 --- a/test/with_dummyserver/test_https.py +++ b/test/with_dummyserver/test_https.py @@ -324,6 +324,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',