diff --git a/docker/client.py b/docker/client.py index a1c8ed26..83eebdd4 100644 --- a/docker/client.py +++ b/docker/client.py @@ -24,8 +24,10 @@ import six from .auth import auth from .unixconn import unixconn +from .ssladapter import ssladapter from .utils import utils from . import errors +from .tls import TLSConfig if not six.PY3: import websocket @@ -37,10 +39,15 @@ STREAM_HEADER_SIZE_BYTES = 8 class Client(requests.Session): def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, - timeout=DEFAULT_TIMEOUT_SECONDS): + timeout=DEFAULT_TIMEOUT_SECONDS, tls=False): super(Client, self).__init__() + if base_url is None: base_url = "http+unix://var/run/docker.sock" + if tls and not base_url.startswith('https://'): + raise errors.TLSParameterError( + 'If using TLS, the base_url argument must begin with ' + '"https://".') if 'unix:///' in base_url: base_url = base_url.replace('unix:/', 'unix:') if base_url.startswith('unix:'): @@ -54,7 +61,13 @@ class Client(requests.Session): self._timeout = timeout self._auth_configs = auth.load_config() - self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) + """ Use SSLAdapter for the ability to specify SSL version """ + if isinstance(tls, TLSConfig): + tls.configure_client(self) + elif tls: + self.mount('https://', ssladapter.SSLAdapter(self.ssl_version)) + else: + self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) def _set_request_timeout(self, kwargs): """Prepare the kwargs for an HTTP request by inserting the timeout diff --git a/docker/errors.py b/docker/errors.py index 85a6d452..749facff 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -63,3 +63,14 @@ class InvalidConfigFile(DockerException): class DeprecatedMethod(DockerException): pass + + +class TLSParameterError(DockerException): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + (". TLS configurations should map the Docker CLI " + "client configurations. See " + "http://docs.docker.com/examples/https/ for " + "API details.") diff --git a/docker/ssladapter/__init__.py b/docker/ssladapter/__init__.py new file mode 100644 index 00000000..1a5e1bb6 --- /dev/null +++ b/docker/ssladapter/__init__.py @@ -0,0 +1 @@ +from .ssladapter import SSLAdapter # flake8: noqa diff --git a/docker/ssladapter/ssladapter.py b/docker/ssladapter/ssladapter.py new file mode 100644 index 00000000..c38481fa --- /dev/null +++ b/docker/ssladapter/ssladapter.py @@ -0,0 +1,22 @@ +""" Resolves OpenSSL issues in some servers: + https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/ + https://github.com/kennethreitz/requests/pull/799 +""" +from requests.adapters import HTTPAdapter +try: + from requests.packages.urllib3.poolmanager import PoolManager +except ImportError: + from urllib3.poolmanager import PoolManager + + +class SSLAdapter(HTTPAdapter): + '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' + def __init__(self, ssl_version=None, **kwargs): + self.ssl_version = ssl_version + super(SSLAdapter, self).__init__(**kwargs) + + def init_poolmanager(self, connections, maxsize, block=False): + self.poolmanager = PoolManager(num_pools=connections, + maxsize=maxsize, + block=block, + ssl_version=self.ssl_version) diff --git a/docker/tls.py b/docker/tls.py new file mode 100644 index 00000000..0cf8acc2 --- /dev/null +++ b/docker/tls.py @@ -0,0 +1,51 @@ +import os + +from . import errors +from .ssladapter import ssladapter + + +class TLSConfig(object): + def __init__(self, tls, tls_cert=None, tls_key=None, tls_verify=False, + tls_ca_cert=None, ssl_version=None): + # Argument compatibility/mapping with + # http://docs.docker.com/examples/https/ + # This diverges from the Docker CLI in that users can specify 'tls' + # here, but also disable any public/default CA pool verification by + # leaving tls_verify=False + + # urllib3 sets a default ssl_version if ssl_version is None + # http://tinyurl.com/kxga8hb + self.ssl_version = ssl_version + + # "tls" and "tls_verify" must have both or neither cert/key files + # In either case, Alert the user when both are expected, but any are + # missing. + + if tls_cert or tls_key: + if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or + not os.path.isfile(tls_key)): + raise errors.TLSParameterError( + 'You must provide either both "tls_cert"/"tls_key" files, ' + 'or neither, in order to use TLS.') + self.cert = (tls_cert, tls_key) + + # Either set tls_verify to True (public/default CA checks) or to the + # path of a CA Cert file. + if tls_verify: + if not tls_ca_cert: + self.verify = True + elif os.path.isfile(tls_ca_cert): + self.verify = tls_ca_cert + else: + raise errors.TLSParameterError( + 'If "tls_verify" is set, then "tls_ca_cert" must be blank' + ' (to check public CA list) OR a path to a Cert File.' + ) + else: + self.verify = False + + def configure_client(self, client): + client.verify = self.verify + client.ssl_version = self.ssl_version + client.cert = self.cert + self.mount('https://', ssladapter.SSLAdapter(self.ssl_version)) diff --git a/setup.py b/setup.py index 7c2bdd8a..6c881a82 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ setup( name="docker-py", version=version, description="Python client for Docker.", - packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils'], + packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils', + 'docker.ssladapter'], install_requires=requirements + test_requirements, zip_safe=False, test_suite='tests',