Skip to content

Commit f302756

Browse files
committed
Rewrite utils.parse_host to detect more invalid addresses.
The method now uses parsing methods from urllib to better split provided URLs. Addresses containing query strings, parameters, passwords or fragments no longer fail silently. SSH addresses containing paths are no longer accepted. Signed-off-by: Joffrey F <[email protected]>
1 parent 6bfe200 commit f302756

File tree

3 files changed

+83
-64
lines changed

3 files changed

+83
-64
lines changed

docker/transport/sshconn.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import urllib.parse
2-
31
import paramiko
42
import requests.adapters
53
import six
64

7-
85
from .. import constants
96

107
if six.PY3:
@@ -82,7 +79,7 @@ def __init__(self, base_url, timeout=60,
8279
self.ssh_client = paramiko.SSHClient()
8380
self.ssh_client.load_system_host_keys()
8481

85-
parsed = urllib.parse.urlparse(base_url)
82+
parsed = six.moves.urllib_parse.urlparse(base_url)
8683
self.ssh_client.connect(
8784
parsed.hostname, parsed.port, parsed.username,
8885
)

docker/utils/utils.py

Lines changed: 72 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import base64
2+
import json
23
import os
34
import os.path
4-
import json
55
import shlex
6-
from distutils.version import StrictVersion
6+
import string
77
from datetime import datetime
8+
from distutils.version import StrictVersion
89

910
import six
1011

@@ -13,11 +14,12 @@
1314

1415
if six.PY2:
1516
from urllib import splitnport
17+
from urlparse import urlparse
1618
else:
17-
from urllib.parse import splitnport
19+
from urllib.parse import splitnport, urlparse
1820

1921
DEFAULT_HTTP_HOST = "127.0.0.1"
20-
DEFAULT_UNIX_SOCKET = "http+unix://var/run/docker.sock"
22+
DEFAULT_UNIX_SOCKET = "http+unix:///var/run/docker.sock"
2123
DEFAULT_NPIPE = 'npipe:////./pipe/docker_engine'
2224

2325
BYTE_UNITS = {
@@ -212,81 +214,93 @@ def parse_repository_tag(repo_name):
212214
return repo_name, None
213215

214216

215-
# Based on utils.go:ParseHost http://tinyurl.com/nkahcfh
216-
# fd:// protocol unsupported (for obvious reasons)
217-
# Added support for http and https
218-
# Protocol translation: tcp -> http, unix -> http+unix
219217
def parse_host(addr, is_win32=False, tls=False):
220-
proto = "http+unix"
221-
port = None
222218
path = ''
219+
port = None
220+
host = None
223221

222+
# Sensible defaults
224223
if not addr and is_win32:
225-
addr = DEFAULT_NPIPE
226-
224+
return DEFAULT_NPIPE
227225
if not addr or addr.strip() == 'unix://':
228226
return DEFAULT_UNIX_SOCKET
229227

230228
addr = addr.strip()
231-
if addr.startswith('http://'):
232-
addr = addr.replace('http://', 'tcp://')
233-
if addr.startswith('http+unix://'):
234-
addr = addr.replace('http+unix://', 'unix://')
235229

236-
if addr == 'tcp://':
230+
parsed_url = urlparse(addr)
231+
proto = parsed_url.scheme
232+
if not proto or any([x not in string.ascii_letters + '+' for x in proto]):
233+
# https://bugs.python.org/issue754016
234+
parsed_url = urlparse('//' + addr, 'tcp')
235+
proto = 'tcp'
236+
237+
if proto == 'fd':
238+
raise errors.DockerException('fd protocol is not implemented')
239+
240+
# These protos are valid aliases for our library but not for the
241+
# official spec
242+
if proto == 'http' or proto == 'https':
243+
tls = proto == 'https'
244+
proto = 'tcp'
245+
elif proto == 'http+unix':
246+
proto = 'unix'
247+
248+
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
237249
raise errors.DockerException(
238-
"Invalid bind address format: {0}".format(addr)
250+
"Invalid bind address protocol: {}".format(addr)
239251
)
240-
elif addr.startswith('unix://'):
241-
addr = addr[7:]
242-
elif addr.startswith('tcp://'):
243-
proto = 'http{0}'.format('s' if tls else '')
244-
addr = addr[6:]
245-
elif addr.startswith('https://'):
246-
proto = "https"
247-
addr = addr[8:]
248-
elif addr.startswith('npipe://'):
249-
proto = 'npipe'
250-
addr = addr[8:]
251-
elif addr.startswith('fd://'):
252-
raise errors.DockerException("fd protocol is not implemented")
253-
elif addr.startswith('ssh://'):
254-
proto = 'ssh'
255-
addr = addr[6:]
256-
else:
257-
if "://" in addr:
258-
raise errors.DockerException(
259-
"Invalid bind address protocol: {0}".format(addr)
260-
)
261-
proto = "https" if tls else "http"
262252

263-
if proto in ("http", "https", "ssh"):
264-
address_parts = addr.split('/', 1)
265-
host = address_parts[0]
266-
if len(address_parts) == 2:
267-
path = '/' + address_parts[1]
268-
host, port = splitnport(host)
253+
if proto == 'tcp' and not parsed_url.netloc:
254+
# "tcp://" is exceptionally disallowed by convention;
255+
# omitting a hostname for other protocols is fine
256+
raise errors.DockerException(
257+
'Invalid bind address format: {}'.format(addr)
258+
)
269259

260+
if any([
261+
parsed_url.params, parsed_url.query, parsed_url.fragment,
262+
parsed_url.password
263+
]):
264+
raise errors.DockerException(
265+
'Invalid bind address format: {}'.format(addr)
266+
)
267+
268+
if parsed_url.path and proto == 'ssh':
269+
raise errors.DockerException(
270+
'Invalid bind address format: no path allowed for this protocol:'
271+
' {}'.format(addr)
272+
)
273+
else:
274+
path = parsed_url.path
275+
if proto == 'unix' and parsed_url.hostname is not None:
276+
# For legacy reasons, we consider unix://path
277+
# to be valid and equivalent to unix:///path
278+
path = '/'.join((parsed_url.hostname, path))
279+
280+
if proto in ('tcp', 'ssh'):
281+
# parsed_url.hostname strips brackets from IPv6 addresses,
282+
# which can be problematic hence our use of splitnport() instead.
283+
host, port = splitnport(parsed_url.netloc)
270284
if port is None or port < 0:
271-
if proto == 'ssh':
272-
port = 22
273-
else:
285+
if proto != 'ssh':
274286
raise errors.DockerException(
275-
"Invalid port: {0}".format(addr)
287+
'Invalid bind address format: port is required:'
288+
' {}'.format(addr)
276289
)
290+
port = 22
277291

278292
if not host:
279293
host = DEFAULT_HTTP_HOST
280-
else:
281-
host = addr
282294

283-
if proto in ("http", "https") and port == -1:
284-
raise errors.DockerException(
285-
"Bind address needs a port: {0}".format(addr))
295+
# Rewrite schemes to fit library internals (requests adapters)
296+
if proto == 'tcp':
297+
proto = 'http{}'.format('s' if tls else '')
298+
elif proto == 'unix':
299+
proto = 'http+unix'
286300

287-
if proto == "http+unix" or proto == 'npipe':
288-
return "{0}://{1}".format(proto, host).rstrip('/')
289-
return "{0}://{1}:{2}{3}".format(proto, host, port, path).rstrip('/')
301+
if proto in ('http+unix', 'npipe'):
302+
return "{}://{}".format(proto, path).rstrip('/')
303+
return '{0}://{1}:{2}{3}'.format(proto, host, port, path).rstrip('/')
290304

291305

292306
def parse_devices(devices):

tests/unit/utils_test.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,11 @@ def test_parse_host(self):
272272
'tcp://',
273273
'udp://127.0.0.1',
274274
'udp://127.0.0.1:2375',
275+
'ssh://:22/path',
276+
'tcp://netloc:3333/path?q=1',
277+
'unix:///sock/path#fragment',
278+
'https://netloc:3333/path;params',
279+
'ssh://:clearpassword@host:22',
275280
]
276281

277282
valid_hosts = {
@@ -281,7 +286,7 @@ def test_parse_host(self):
281286
'http://:7777': 'http://127.0.0.1:7777',
282287
'https://kokia.jp:2375': 'https://kokia.jp:2375',
283288
'unix:///var/run/docker.sock': 'http+unix:///var/run/docker.sock',
284-
'unix://': 'http+unix://var/run/docker.sock',
289+
'unix://': 'http+unix:///var/run/docker.sock',
285290
'12.234.45.127:2375/docker/engine': (
286291
'http://12.234.45.127:2375/docker/engine'
287292
),
@@ -294,6 +299,9 @@ def test_parse_host(self):
294299
'[fd12::82d1]:2375/docker/engine': (
295300
'http://[fd12::82d1]:2375/docker/engine'
296301
),
302+
'ssh://': 'ssh://127.0.0.1:22',
303+
'ssh://user@localhost:22': 'ssh://user@localhost:22',
304+
'ssh://user@remote': 'ssh://user@remote:22',
297305
}
298306

299307
for host in invalid_hosts:
@@ -304,7 +312,7 @@ def test_parse_host(self):
304312
assert parse_host(host, None) == expected
305313

306314
def test_parse_host_empty_value(self):
307-
unix_socket = 'http+unix://var/run/docker.sock'
315+
unix_socket = 'http+unix:///var/run/docker.sock'
308316
npipe = 'npipe:////./pipe/docker_engine'
309317

310318
for val in [None, '']:

0 commit comments

Comments
 (0)