Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ MAINTAINER Joffrey F <joffrey@docker.com>
RUN mkdir /home/docker-py
WORKDIR /home/docker-py

ADD requirements.txt /home/docker-py/requirements.txt
COPY requirements.txt /home/docker-py/requirements.txt
RUN pip install -r requirements.txt

ADD test-requirements.txt /home/docker-py/test-requirements.txt
COPY test-requirements.txt /home/docker-py/test-requirements.txt
RUN pip install -r test-requirements.txt

ADD . /home/docker-py
COPY . /home/docker-py
RUN pip install .
6 changes: 3 additions & 3 deletions Dockerfile-py3
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ MAINTAINER Joffrey F <joffrey@docker.com>
RUN mkdir /home/docker-py
WORKDIR /home/docker-py

ADD requirements.txt /home/docker-py/requirements.txt
COPY requirements3.txt /home/docker-py/requirements.txt
RUN pip install -r requirements.txt

ADD test-requirements.txt /home/docker-py/test-requirements.txt
COPY test-requirements.txt /home/docker-py/test-requirements.txt
RUN pip install -r test-requirements.txt

ADD . /home/docker-py
COPY . /home/docker-py
RUN pip install .
130 changes: 130 additions & 0 deletions docker/ssladapter/ssl_match_hostname.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Slightly modified version of match_hostname in python's ssl library
# https://hg.python.org/cpython/file/tip/Lib/ssl.py
# Changed to make code python 2.x compatible (unicode strings for ip_address
# and 3.5-specific var assignment syntax)

import ipaddress
import re

try:
from ssl import CertificateError
except ImportError:
CertificateError = ValueError

import six


def _ipaddress_match(ipname, host_ip):
"""Exact matching of IP addresses.

RFC 6125 explicitly doesn't define an algorithm for this
(section 1.7.2 - "Out of Scope").
"""
# OpenSSL may add a trailing newline to a subjectAltName's IP address
ip = ipaddress.ip_address(six.text_type(ipname.rstrip()))
return ip == host_ip


def _dnsname_match(dn, hostname, max_wildcards=1):
"""Matching according to RFC 6125, section 6.4.3

http://tools.ietf.org/html/rfc6125#section-6.4.3
"""
pats = []
if not dn:
return False

split_dn = dn.split(r'.')
leftmost, remainder = split_dn[0], split_dn[1:]

wildcards = leftmost.count('*')
if wildcards > max_wildcards:
# Issue #17980: avoid denials of service by refusing more
# than one wildcard per fragment. A survey of established
# policy among SSL implementations showed it to be a
# reasonable choice.
raise CertificateError(
"too many wildcards in certificate DNS name: " + repr(dn))

# speed up common case w/o wildcards
if not wildcards:
return dn.lower() == hostname.lower()

# RFC 6125, section 6.4.3, subitem 1.
# The client SHOULD NOT attempt to match a presented identifier in which
# the wildcard character comprises a label other than the left-most label.
if leftmost == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
# RFC 6125, section 6.4.3, subitem 3.
# The client SHOULD NOT attempt to match a presented identifier
# where the wildcard character is embedded within an A-label or
# U-label of an internationalized domain name.
pats.append(re.escape(leftmost))
else:
# Otherwise, '*' matches any dotless string, e.g. www*
pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))

# add the remaining fragments, ignore any wildcards
for frag in remainder:
pats.append(re.escape(frag))

pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
return pat.match(hostname)


def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
rules are followed, but IP addresses are not accepted for *hostname*.

CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate, match_hostname needs a "
"SSL socket or SSL context with either "
"CERT_OPTIONAL or CERT_REQUIRED")
try:
host_ip = ipaddress.ip_address(six.text_type(hostname))
except ValueError:
# Not an IP address (common case)
host_ip = None
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if host_ip is None and _dnsname_match(value, hostname):
return
dnsnames.append(value)
elif key == 'IP Address':
if host_ip is not None and _ipaddress_match(value, host_ip):
return
dnsnames.append(value)
if not dnsnames:
# The subject is only checked when there is no dNSName entry
# in subjectAltName
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_match(value, hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError(
"hostname %r doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError(
"hostname %r doesn't match %r"
% (hostname, dnsnames[0])
)
else:
raise CertificateError(
"no appropriate commonName or "
"subjectAltName fields were found"
)
9 changes: 9 additions & 0 deletions docker/ssladapter/ssladapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
https://github.com/kennethreitz/requests/pull/799
"""
import sys

from distutils.version import StrictVersion
from requests.adapters import HTTPAdapter

Expand All @@ -10,8 +12,15 @@
except ImportError:
import urllib3


PoolManager = urllib3.poolmanager.PoolManager

# Monkey-patching match_hostname with a version that supports
# IP-address checking. Not necessary for Python 3.5 and above
if sys.version_info[0] < 3 or sys.version_info[1] < 5:
from .ssl_match_hostname import match_hostname
urllib3.connection.match_hostname = match_hostname


class SSLAdapter(HTTPAdapter):
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
Expand Down
6 changes: 4 additions & 2 deletions docker/unixconn/unixconn.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
class UnixHTTPConnection(httplib.HTTPConnection, object):
def __init__(self, base_url, unix_socket, timeout=60):
super(UnixHTTPConnection, self).__init__(
'localhost', timeout=timeout)
'localhost', timeout=timeout
)
self.base_url = base_url
self.unix_socket = unix_socket
self.timeout = timeout
Expand All @@ -46,7 +47,8 @@ def connect(self):
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60):
super(UnixHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout)
'localhost', timeout=timeout
)
self.base_url = base_url
self.socket_path = socket_path
self.timeout = timeout
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
requests==2.5.3
six>=1.4.0
websocket-client==0.32.0
py2-ipaddress==3.4.1
3 changes: 3 additions & 0 deletions requirements3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
requests==2.5.3
six>=1.4.0
websocket-client==0.32.0
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
'websocket-client >= 0.32.0',
]

if sys.version_info[0] == 2:
requirements.append('py2-ipaddress >= 3.4.1')

exec(open('docker/version.py').read())

with open('./test-requirements.txt') as test_reqs_txt:
Expand Down
73 changes: 73 additions & 0 deletions tests/unit/ssladapter_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from docker.ssladapter import ssladapter
from docker.ssladapter.ssl_match_hostname import (
match_hostname, CertificateError
)

try:
from ssl import OP_NO_SSLv3, OP_NO_SSLv2, OP_NO_TLSv1
except ImportError:
OP_NO_SSLv2 = 0x1000000
OP_NO_SSLv3 = 0x2000000
OP_NO_TLSv1 = 0x4000000

from .. import base


class SSLAdapterTest(base.BaseTestCase):
def test_only_uses_tls(self):
ssl_context = ssladapter.urllib3.util.ssl_.create_urllib3_context()

assert ssl_context.options & OP_NO_SSLv3
assert ssl_context.options & OP_NO_SSLv2
assert not ssl_context.options & OP_NO_TLSv1


class MatchHostnameTest(base.BaseTestCase):
cert = {
'issuer': (
(('countryName', u'US'),),
(('stateOrProvinceName', u'California'),),
(('localityName', u'San Francisco'),),
(('organizationName', u'Docker Inc'),),
(('organizationalUnitName', u'Docker-Python'),),
(('commonName', u'localhost'),),
(('emailAddress', u'info@docker.com'),)
),
'notAfter': 'Mar 25 23:08:23 2030 GMT',
'notBefore': u'Mar 25 23:08:23 2016 GMT',
'serialNumber': u'BD5F894C839C548F',
'subject': (
(('countryName', u'US'),),
(('stateOrProvinceName', u'California'),),
(('localityName', u'San Francisco'),),
(('organizationName', u'Docker Inc'),),
(('organizationalUnitName', u'Docker-Python'),),
(('commonName', u'localhost'),),
(('emailAddress', u'info@docker.com'),)
),
'subjectAltName': (
('DNS', u'localhost'),
('DNS', u'*.gensokyo.jp'),
('IP Address', u'127.0.0.1'),
),
'version': 3
}

def test_match_ip_address_success(self):
assert match_hostname(self.cert, '127.0.0.1') is None

def test_match_localhost_success(self):
assert match_hostname(self.cert, 'localhost') is None

def test_match_dns_success(self):
assert match_hostname(self.cert, 'touhou.gensokyo.jp') is None

def test_match_ip_address_failure(self):
self.assertRaises(
CertificateError, match_hostname, self.cert, '192.168.0.25'
)

def test_match_dns_failure(self):
self.assertRaises(
CertificateError, match_hostname, self.cert, 'foobar.co.uk'
)
17 changes: 0 additions & 17 deletions tests/unit/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,9 @@
import pytest
import six

try:
from ssl import OP_NO_SSLv3, OP_NO_SSLv2, OP_NO_TLSv1
except ImportError:
OP_NO_SSLv2 = 0x1000000
OP_NO_SSLv3 = 0x2000000
OP_NO_TLSv1 = 0x4000000

from docker.client import Client
from docker.constants import DEFAULT_DOCKER_API_VERSION
from docker.errors import DockerException, InvalidVersion
from docker.ssladapter import ssladapter
from docker.utils import (
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file,
Expand Down Expand Up @@ -962,12 +954,3 @@ def test_tar_with_directory_symlinks(self):
self.assertEqual(
sorted(tar_data.getnames()), ['bar', 'bar/foo', 'foo']
)


class SSLAdapterTest(base.BaseTestCase):
def test_only_uses_tls(self):
ssl_context = ssladapter.urllib3.util.ssl_.create_urllib3_context()

assert ssl_context.options & OP_NO_SSLv3
assert ssl_context.options & OP_NO_SSLv2
assert not ssl_context.options & OP_NO_TLSv1
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ commands =
py.test --cov=docker {posargs:tests/unit}
deps =
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt
{py26,py27}: -r{toxinidir}/requirements.txt
{py33,py34}: -r{toxinidir}/requirements3.txt

[testenv:flake8]
commands = flake8 docker tests
Expand Down