Repository: urllib3/urllib3

Issue:
Warn if we do not have access to SNI
Suggested in kennethreitz/requests#2910. This would make it easier for people to correlate unexpected cert verification failures with the specific action they need to take. 



Discussion:
+1 to more descriptive errors, ideally it should only affect when there is no SNI and SNI is required for the request to succeed.

Well, we can't really tell when SNI is required for the request to succeed, only when it's _likely_ to be needed. In that case, I think warning once, on the first HTTPS request, on a platform that has no SNI is likely to be the way to go here.


Resolving patch:
diff --git a/urllib3/__init__.py b/urllib3/__init__.py
--- a/urllib3/__init__.py
+++ b/urllib3/__init__.py
@@ -81,6 +81,8 @@ def add_stderr_logger(level=logging.DEBUG):
 # InsecurePlatformWarning's don't vary between requests, so we keep it default.
 warnings.simplefilter('default', exceptions.InsecurePlatformWarning,
                       append=True)
+# SNIMissingWarnings should go off only once.
+warnings.simplefilter('default', exceptions.SNIMissingWarning)
 
 
 def disable_warnings(category=exceptions.HTTPWarning):
diff --git a/urllib3/exceptions.py b/urllib3/exceptions.py
--- a/urllib3/exceptions.py
+++ b/urllib3/exceptions.py
@@ -175,6 +175,11 @@ class InsecurePlatformWarning(SecurityWarning):
     pass
 
 
+class SNIMissingWarning(HTTPWarning):
+    "Warned when making a HTTPS request without SNI available."
+    pass
+
+
 class ResponseNotChunked(ProtocolError, ValueError):
     "Response needs to be chunked in order to read it as chunks."
     pass
diff --git a/urllib3/util/ssl_.py b/urllib3/util/ssl_.py
--- a/urllib3/util/ssl_.py
+++ b/urllib3/util/ssl_.py
@@ -6,7 +6,7 @@
 from binascii import hexlify, unhexlify
 from hashlib import md5, sha1, sha256
 
-from ..exceptions import SSLError, InsecurePlatformWarning
+from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning
 
 
 SSLContext = None
@@ -303,4 +303,15 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
         context.load_cert_chain(certfile, keyfile)
     if HAS_SNI:  # Platform-specific: OpenSSL with enabled SNI
         return context.wrap_socket(sock, server_hostname=server_hostname)
+
+    warnings.warn(
+        'An HTTPS request has been made, but the SNI (Subject Name '
+        'Indication) extension to TLS is not available on this platform. '
+        'This may cause the server to present an incorrect TLS '
+        'certificate, which can cause validation failures. For more '
+        'information, see '
+        'https://urllib3.readthedocs.org/en/latest/security.html'
+        '#snimissingwarning.',
+        SNIMissingWarning
+    )
     return context.wrap_socket(sock)


Test patch:
diff --git a/test/test_util.py b/test/test_util.py
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -26,6 +26,7 @@
     TimeoutStateError,
     InsecureRequestWarning,
     SSLError,
+    SNIMissingWarning,
 )
 
 from urllib3.util import is_fp_closed, ssl_
@@ -415,6 +416,20 @@ def test_ssl_wrap_socket_with_no_sni(self):
         mock_context.wrap_socket.assert_called_once_with(socket)
         ssl_.HAS_SNI = HAS_SNI
 
+    def test_ssl_wrap_socket_with_no_sni_warns(self):
+        socket = object()
+        mock_context = Mock()
+        # Ugly preservation of original value
+        HAS_SNI = ssl_.HAS_SNI
+        ssl_.HAS_SNI = False
+        with patch('warnings.warn') as warn:
+            ssl_wrap_socket(ssl_context=mock_context, sock=socket)
+        mock_context.wrap_socket.assert_called_once_with(socket)
+        ssl_.HAS_SNI = HAS_SNI
+        self.assertTrue(warn.call_count >= 1)
+        warnings = [call[0][1] for call in warn.call_args_list]
+        self.assertTrue(SNIMissingWarning in warnings)
+
     def test_const_compare_digest_fallback(self):
         target = hashlib.sha256(b'abcdef').digest()
         self.assertTrue(_const_compare_digest_backport(target, target))
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
@@ -35,6 +35,7 @@
 )
 from urllib3.packages import six
 from urllib3.util.timeout import Timeout
+from urllib3.util.ssl_ import HAS_SNI
 
 
 ResourceWarning = getattr(
@@ -77,7 +78,10 @@ def test_verified(self):
                 self.assertFalse(warn.called, warn.call_args_list)
             else:
                 self.assertTrue(warn.called)
-                call, = warn.call_args_list
+                if HAS_SNI:
+                    call = warn.call_args_list[0]
+                else:
+                    call = warn.call_args_list[1]
                 error = call[0][1]
                 self.assertEqual(error, InsecurePlatformWarning)
 
@@ -176,8 +180,10 @@ def test_ssl_unverified_with_ca_certs(self):
             calls = warn.call_args_list
             if sys.version_info >= (2, 7, 9):
                 category = calls[0][0][1]
-            else:
+            elif HAS_SNI:
                 category = calls[1][0][1]
+            else:
+                category = calls[2][0][1]
             self.assertEqual(category, InsecureRequestWarning)
 
     @requires_network