diff --git a/src/urllib3/connection.py b/src/urllib3/connection.py index 81474178cc..d3c61bf582 100644 --- a/src/urllib3/connection.py +++ b/src/urllib3/connection.py @@ -398,6 +398,23 @@ def connect(self): ssl_context=context, ) + # If we're using all defaults and the connection + # is TLSv1 or TLSv1.1 we throw a DeprecationWarning + # for the host. + if ( + default_ssl_context + and self.ssl_version is None + and hasattr(self.sock, "version") + and self.sock.version() in {"TLSv1", "TLSv1.1"} + ): + warnings.warn( + "Negotiating TLSv1/TLSv1.1 by default is deprecated " + "and will be disabled in urllib3 v2.0.0. Connecting to " + "'%s' with '%s' can be enabled by explicitly opting-in " + "with 'ssl_version'" % (self.host, self.sock.version()), + DeprecationWarning, + ) + if self.assert_fingerprint: assert_fingerprint( self.sock.getpeercert(binary_form=True), self.assert_fingerprint diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py index 5eb241de6b..c763223ed8 100644 --- a/test/with_dummyserver/test_https.py +++ b/test/with_dummyserver/test_https.py @@ -87,6 +87,9 @@ class TestHTTPS(HTTPSDummyServerTestCase): tls_protocol_name = None + def tls_protocol_deprecated(self): + return self.tls_protocol_name in {"TLSv1", "TLSv1.1"} + @classmethod def setup_class(cls): super(TestHTTPS, cls).setup_class() @@ -213,26 +216,25 @@ def test_verified(self): conn = https_pool._new_conn() assert conn.__class__ == VerifiedHTTPSConnection - with mock.patch("warnings.warn") as warn: + with warnings.catch_warnings(record=True) as w: r = https_pool.request("GET", "/") assert r.status == 200 - # Modern versions of Python, or systems using PyOpenSSL, don't - # emit warnings. - if ( - sys.version_info >= (2, 7, 9) - or util.IS_PYOPENSSL - or util.IS_SECURETRANSPORT - ): - assert not warn.called, warn.call_args_list - else: - assert warn.called - if util.HAS_SNI: - call = warn.call_args_list[0] - else: - call = warn.call_args_list[1] - error = call[0][1] - assert error == InsecurePlatformWarning + # If we're using a deprecated TLS version we can remove 'DeprecationWarning' + if self.tls_protocol_deprecated(): + w = [x for x in w if x.category != DeprecationWarning] + + # Modern versions of Python, or systems using PyOpenSSL, don't + # emit warnings. + if ( + sys.version_info >= (2, 7, 9) + or util.IS_PYOPENSSL + or util.IS_SECURETRANSPORT + ): + assert w == [] + else: + assert len(w) > 1 + assert any(x.category == InsecureRequestWarning for x in w) def test_verified_with_context(self): ctx = util.ssl_.create_urllib3_context(cert_reqs=ssl.CERT_REQUIRED) @@ -306,10 +308,15 @@ def test_ca_dir_verified(self, tmpdir): conn = https_pool._new_conn() assert conn.__class__ == VerifiedHTTPSConnection - with mock.patch("warnings.warn") as warn: + with warnings.catch_warnings(record=True) as w: r = https_pool.request("GET", "/") assert r.status == 200 - assert not warn.called, warn.call_args_list + + # If we're using a deprecated TLS version we can remove 'DeprecationWarning' + if self.tls_protocol_deprecated(): + w = [x for x in w if x.category != DeprecationWarning] + + assert w == [] def test_invalid_common_name(self): with HTTPSConnectionPool( @@ -391,6 +398,11 @@ def test_ssl_unverified_with_ca_certs(self): # the unverified warning. Older systems may also emit other # warnings, which we want to ignore here. calls = warn.call_args_list + + # If we're using a deprecated TLS version we can remove 'DeprecationWarning' + if self.tls_protocol_deprecated(): + calls = [call for call in calls if call[0][1] != DeprecationWarning] + if ( sys.version_info >= (2, 7, 9) or util.IS_PYOPENSSL @@ -665,7 +677,13 @@ def _request_without_resource_warnings(self, method, url): ) as https_pool: https_pool.request(method, url) - return [x for x in w if not isinstance(x.message, ResourceWarning)] + w = [x for x in w if not isinstance(x.message, ResourceWarning)] + + # If we're using a deprecated TLS version we can remove 'DeprecationWarning' + if self.tls_protocol_deprecated(): + w = [x for x in w if x.category != DeprecationWarning] + + return w def test_set_ssl_version_to_tls_version(self): if self.tls_protocol_name is None: @@ -699,6 +717,68 @@ def test_tls_protocol_name_of_socket(self): finally: conn.close() + def test_default_tls_version_deprecations(self): + if self.tls_protocol_name is None: + pytest.skip("Skipping base test class") + + with HTTPSConnectionPool( + self.host, self.port, ca_certs=DEFAULT_CA + ) as https_pool: + conn = https_pool._get_conn() + try: + with warnings.catch_warnings(record=True) as w: + conn.connect() + if not hasattr(conn.sock, "version"): + pytest.skip("SSLSocket.version() not available") + finally: + conn.close() + + if self.tls_protocol_deprecated(): + assert len(w) == 1 + assert str(w[0].message) == ( + "Negotiating TLSv1/TLSv1.1 by default is deprecated " + "and will be disabled in urllib3 v2.0.0. Connecting to " + "'%s' with '%s' can be enabled by explicitly opting-in " + "with 'ssl_version'" % (self.host, self.tls_protocol_name) + ) + else: + assert w == [] + + def test_no_tls_version_deprecation_with_ssl_version(self): + if self.tls_protocol_name is None: + pytest.skip("Skipping base test class") + + with HTTPSConnectionPool( + self.host, self.port, ca_certs=DEFAULT_CA, ssl_version=util.PROTOCOL_TLS + ) as https_pool: + conn = https_pool._get_conn() + try: + with warnings.catch_warnings(record=True) as w: + conn.connect() + finally: + conn.close() + + assert w == [] + + def test_no_tls_version_deprecation_with_ssl_context(self): + if self.tls_protocol_name is None: + pytest.skip("Skipping base test class") + + with HTTPSConnectionPool( + self.host, + self.port, + ca_certs=DEFAULT_CA, + ssl_context=util.ssl_.create_urllib3_context(), + ) as https_pool: + conn = https_pool._get_conn() + try: + with warnings.catch_warnings(record=True) as w: + conn.connect() + finally: + conn.close() + + assert w == [] + @pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8+") def test_sslkeylogfile(self, tmpdir, monkeypatch): if not hasattr(util.SSLContext, "keylog_filename"):