diff --git a/docker/client.py b/docker/client.py index 38355b09c4..d7d130761f 100644 --- a/docker/client.py +++ b/docker/client.py @@ -16,6 +16,7 @@ import re import shlex import struct +import os import requests import requests.exceptions @@ -23,7 +24,9 @@ from .auth import auth from .unixconn import unixconn +from .ssladapter import ssladapter from .utils import utils +from .exceptions import exceptions if not six.PY3: import websocket @@ -69,9 +72,20 @@ def is_server_error(self): class Client(requests.Session): - def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, - timeout=DEFAULT_TIMEOUT_SECONDS): + def __init__(self, + base_url=None, + version=DEFAULT_DOCKER_API_VERSION, + timeout=DEFAULT_TIMEOUT_SECONDS, + tls=False, + tls_cert=None, + tls_key=None, + tls_verify=False, + tls_ca_cert=None, + ssl_version=None): super(Client, self).__init__() + + if (tls or tls_verify) and not base_url.startswith('https://'): + raise exceptions.TLSParameterError('If using TLS, the base_url argument must begin with "https://".') if base_url is None: base_url = "http+unix://var/run/docker.sock" if 'unix:///' in base_url: @@ -87,7 +101,46 @@ def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, self._timeout = timeout self._auth_configs = auth.load_config() - self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) + """ Argument compatibility/mapping with http://docs.docker.io/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 + https://github.com/shazow/urllib3/blob/62ecd1523ec383802cb13b09bd7084d2da997420/urllib3/util/ssl_.py#L83 + """ + 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 or tls_verify) and (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 exceptions.TLSParameterError( + 'You must provide either both "tls_cert"/"tls_key" files, or neither, in order to use TLS.') + else: + 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. + ref: https://github.com/kennethreitz/requests/blob/739d153ef77765392fa109bebead4260c05f3193/requests/adapters.py#L135-L137 + ref: https://github.com/kennethreitz/requests/blob/master/requests/sessions.py#L433-L439 + """ + 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 exceptions.TLSParameterError( + 'If "tls_verify" is set, then "tls_ca_cert" must be blank (to check default/public CA list) OR a path to a CA Cert File.') + else: + self.verify = False + + """ Use SSLAdapter for the ability to specify SSL version """ + if tls or tls_verify: + 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/exceptions/__init__.py b/docker/exceptions/__init__.py new file mode 100644 index 0000000000..fdb8e77976 --- /dev/null +++ b/docker/exceptions/__init__.py @@ -0,0 +1 @@ +from .exceptions import TLSParameterError diff --git a/docker/exceptions/exceptions.py b/docker/exceptions/exceptions.py new file mode 100644 index 0000000000..a22231a902 --- /dev/null +++ b/docker/exceptions/exceptions.py @@ -0,0 +1,6 @@ +class TLSParameterError(ValueError): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + "\n\nTLS configurations should map the Docker CLI client configurations. See http://docs.docker.io/examples/https/ for API details." diff --git a/docker/ssladapter/__init__.py b/docker/ssladapter/__init__.py new file mode 100644 index 0000000000..182c35c581 --- /dev/null +++ b/docker/ssladapter/__init__.py @@ -0,0 +1 @@ +from .ssladapter import SSLAdapter diff --git a/docker/ssladapter/ssladapter.py b/docker/ssladapter/ssladapter.py new file mode 100644 index 0000000000..b78223ad3d --- /dev/null +++ b/docker/ssladapter/ssladapter.py @@ -0,0 +1,23 @@ +""" 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) \ No newline at end of file diff --git a/setup.py b/setup.py index 8c196f89d9..40e31c8427 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ name="docker-py", version='0.3.0', description="Python client for Docker.", - packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils'], + packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils', 'docker.ssladapter', 'docker.exceptions'], install_requires=requirements + test_requirements, zip_safe=False, test_suite='tests',