Repository: smarter-travel-media/warthog

Issue:
Remove dependency on `ssl` module protocol constants in warthog.confg
Like the issue in https://github.com/smarter-travel-media/warthog/commit/a3c6ed378ff3c83133ca18898644eb356d203067 we need to remove dependence on the `ssl` module in `warthog.confg` to allow use on Python 2.6 

Comments:

Patch:
diff --git a/warthog/config.py b/warthog/config.py
index 3dfab6f..9c72399 100644
--- a/warthog/config.py
+++ b/warthog/config.py
@@ -15,15 +15,14 @@ Load and parse configuration for a client from an INI-style file.
 """
 
 import collections
-import threading
-import ssl
 import sys
-
+import threading
 import codecs
 import os.path
+
 import warthog.exceptions
+import warthog.ssl
 from .packages import six
-# pylint: disable=import-error
 from .packages.six.moves import configparser
 
 # List of locations (from most preferred to least preferred) that will
@@ -38,12 +37,10 @@ DEFAULT_CONFIG_LOCATIONS = [
     os.path.join(os.getcwd(), 'warthog.ini')
 ]
 
-
 # By default, we assume that the configuration file is in UTF-8 unless
 # the caller indicates it is in some other encoding.
 DEFAULT_CONFIG_ENCODING = 'utf-8'
 
-
 # Simple immutable struct to hold configuration information for a WarthogClient
 WarthogConfigSettings = collections.namedtuple(
     'WarthogConfigSettings', ['scheme_host', 'username', 'password', 'verify', 'ssl_version'])
@@ -163,10 +160,14 @@ class WarthogConfigLoader(object):
 
 
 def parse_ssl_version(version_str, ssl_module=None):
-    """Get the :mod:`ssl` protocol constant that represents the given version
+    """Get the :mod:`warthog.ssl` protocol constant that represents the given version
     string if it exists, raising an error if the version string is malformed or
     does not correspond to a supported protocol.
 
+    Note that the :mod:`warthog.ssl` protocol constants should match the Python
+    :mod:`ssl` module exactly. The difference is that our SSL module has all
+    potential versions while older Python modules did not.
+
     :param unicode version_str: Version string to resolve to a protocol
     :param module ssl_module: SSL module to get the protocol constant from
     :return: The ssl module protocol constant or ``None``
@@ -180,7 +181,7 @@ def parse_ssl_version(version_str, ssl_module=None):
     if not version_str:
         return None
 
-    ssl_module = ssl_module if ssl_module is not None else ssl
+    ssl_module = ssl_module if ssl_module is not None else warthog.ssl
 
     # Get a list of all the 'PROTOCOL' constants in the SSL module, and
     # strip the 'PROTOCOL_' prefix. This is the set of supported SSL or
diff --git a/warthog/ssl.py b/warthog/ssl.py
new file mode 100644
index 0000000..dea969f
--- /dev/null
+++ b/warthog/ssl.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Warthog - Simple client for A10 load balancers
+#
+# Copyright 2014-2016 Smarter Travel
+#
+# Available under the MIT license. See LICENSE for details.
+#
+
+"""
+warthog.ssl
+~~~~~~~~~~~
+
+SSL related constants used by Warthog
+"""
+
+# Define our own versions of expected constants in the Python ssl
+# module since older Python versions didn't define all of them. For
+# example Python 2.6 and Python 3.3 don't include TLSv1.1 or TLSv1.2
+# and we need to support the combination of those Python versions
+# and TLS versions. Kinda hacky but required. Such is life.
+
+PROTOCOL_SSLv3 = 1
+
+PROTOCOL_SSLv23 = 2
+
+PROTOCOL_TLSv1 = 3
+
+PROTOCOL_TLSv1_1 = 4
+
+PROTOCOL_TLSv1_2 = 5
diff --git a/warthog/transport.py b/warthog/transport.py
index 836a526..776d2c8 100644
--- a/warthog/transport.py
+++ b/warthog/transport.py
@@ -26,17 +26,12 @@ from requests.adapters import (
 from requests.packages.urllib3.exceptions import InsecureRequestWarning
 from requests.packages.urllib3.poolmanager import PoolManager
 
-# HACK: We need to default to TLSv1.2 to work with the new load balancer
-# but Python 2.6 and Python 3.3 don't have the TLSv1.2 constant. BUT, TLS
-# version 1.2 will work with the version of requests we use on Python 2.6
-# so we hack in the constant here for the sake of a default.
-# pylint: disable=invalid-name
-_PROTOCOL_TLSv1_2 = 5
+import warthog.ssl
 
 # Default to using the SSL/TLS version that the A10 requires instead of
 # the default that the requests/urllib3 library picks. Or, maybe the A10
 # just doesn't allow the client to negotiate. Either way, we use TLSv1.2.
-DEFAULT_SSL_VERSION = _PROTOCOL_TLSv1_2
+DEFAULT_SSL_VERSION = warthog.ssl.PROTOCOL_TLSv1_2
 
 # Default to verifying SSL/TLS certs because "safe by default" is a good idea.
 DEFAULT_CERT_VERIFY = True


Test patch:
diff --git a/test/test_ssl.py b/test/test_ssl.py
new file mode 100644
index 0000000..9165683
--- /dev/null
+++ b/test/test_ssl.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+
+import ssl
+
+import warthog.ssl
+
+
+# Test our hacky constants to make sure we haven't shot ourselves in the
+# foot in a completely obvious and predictable way.
+
+
+def test_ssl3_matches():
+    assert ssl.PROTOCOL_SSLv3 == warthog.ssl.PROTOCOL_SSLv3
+
+
+def test_ssl23_matches():
+    assert ssl.PROTOCOL_SSLv23 == warthog.ssl.PROTOCOL_SSLv23
+
+
+def test_tls1_matches():
+    assert ssl.PROTOCOL_TLSv1 == warthog.ssl.PROTOCOL_TLSv1
+
+
+def test_tls1_1_matches():
+    try:
+        # It's possible that we're running under an old version of Python
+        # and this constant doesn't exist (hence why warthog.ssl exists).
+        module_const = ssl.PROTOCOL_TLSv1_1
+    except AttributeError:
+        return
+
+    assert module_const == warthog.ssl.PROTOCOL_TLSv1_1
+
+
+def test_tls1_2_matches():
+    try:
+        # It's possible that we're running under an old version of Python
+        # and this constant doesn't exist (hence why warthog.ssl exists).
+        module_const = ssl.PROTOCOL_TLSv1_2
+    except AttributeError:
+        return
+
+    assert module_const == warthog.ssl.PROTOCOL_TLSv1_2
diff --git a/test/test_transport.py b/test/test_transport.py
index fd5d81a..3d399e6 100644
--- a/test/test_transport.py
+++ b/test/test_transport.py
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 
-import ssl
-
+import warthog.ssl
 import warthog.transport
 
 
@@ -16,11 +15,11 @@ def test_get_transport_factory_no_verify():
 
 
 def test_get_transport_factory_alternate_ssl_version():
-    factory = warthog.transport.get_transport_factory(ssl_version=ssl.PROTOCOL_SSLv3)
+    factory = warthog.transport.get_transport_factory(ssl_version=warthog.ssl.PROTOCOL_TLSv1_1)
     session = factory()
     adapter = session.get_adapter('https://lb.example.com')
 
-    assert ssl.PROTOCOL_SSLv3 == adapter.ssl_version, 'Did not get expected SSL version'
+    assert warthog.ssl.PROTOCOL_TLSv1_1 == adapter.ssl_version, 'Did not get expected SSL version'
 
 
 def test_get_transport_factory_with_defaults():
@@ -31,18 +30,3 @@ def test_get_transport_factory_with_defaults():
     assert warthog.transport.DEFAULT_SSL_VERSION == adapter.ssl_version, 'Did not get default TLS version'
     assert warthog.transport.DEFAULT_CERT_VERIFY == session.verify, 'Did not get default verify setting'
 
-
-def test_default_tls_version_matches_ssl_module():
-    try:
-        import ssl
-        module_version = ssl.PROTOCOL_TLSv1_2
-    except AttributeError:
-        # Running an old version of Python that doesn't have the version
-        # constant. This is the reason we need to use our own and we can't
-        # verify that it's right here so just end.
-        return
-
-    # Make sure that our default version matches the actual constant in the
-    # ssl module. This is really just a sanity check to make sure this hack
-    # doesn't blow up in our face
-    assert module_version == warthog.transport.DEFAULT_SSL_VERSION