Repository: urllib3/urllib3

Issue:
[v1.26] Deprecate TLS <1.2 on v1.26.x
If we detect "TLSv1" or "TLSv1.1" from any `SSLSocket.version()` we should be emitting a `DeprecationWarning` including the host


Discussion:

Resolving patch:
diff --git a/src/urllib3/connection.py b/src/urllib3/connection.py
--- 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


Test patch:
diff --git a/test/with_dummyserver/test_https.py b/test/with_dummyserver/test_https.py
--- 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"):