Skip to content

Commit 787f3f5

Browse files
authored
Merge pull request #1079 from docker/1024-npipe-support
npipe support
2 parents 080b471 + a8746f7 commit 787f3f5

File tree

11 files changed

+322
-14
lines changed

11 files changed

+322
-14
lines changed

docker/client.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import json
1616
import struct
17-
import sys
1817

1918
import requests
2019
import requests.exceptions
@@ -26,10 +25,14 @@
2625
from . import constants
2726
from . import errors
2827
from .auth import auth
29-
from .unixconn import unixconn
3028
from .ssladapter import ssladapter
31-
from .utils import utils, check_resource, update_headers, kwargs_from_env
3229
from .tls import TLSConfig
30+
from .transport import UnixAdapter
31+
from .utils import utils, check_resource, update_headers, kwargs_from_env
32+
try:
33+
from .transport import NpipeAdapter
34+
except ImportError:
35+
pass
3336

3437

3538
def from_env(**kwargs):
@@ -59,11 +62,26 @@ def __init__(self, base_url=None, version=None,
5962

6063
self._auth_configs = auth.load_config()
6164

62-
base_url = utils.parse_host(base_url, sys.platform, tls=bool(tls))
65+
base_url = utils.parse_host(
66+
base_url, constants.IS_WINDOWS_PLATFORM, tls=bool(tls)
67+
)
6368
if base_url.startswith('http+unix://'):
64-
self._custom_adapter = unixconn.UnixAdapter(base_url, timeout)
69+
self._custom_adapter = UnixAdapter(base_url, timeout)
6570
self.mount('http+docker://', self._custom_adapter)
6671
self.base_url = 'http+docker://localunixsocket'
72+
elif base_url.startswith('npipe://'):
73+
if not constants.IS_WINDOWS_PLATFORM:
74+
raise errors.DockerException(
75+
'The npipe:// protocol is only supported on Windows'
76+
)
77+
try:
78+
self._custom_adapter = NpipeAdapter(base_url, timeout)
79+
except NameError:
80+
raise errors.DockerException(
81+
'Install pypiwin32 package to enable npipe:// support'
82+
)
83+
self.mount('http+docker://', self._custom_adapter)
84+
self.base_url = 'http+docker://localnpipe'
6785
else:
6886
# Use SSLAdapter for the ability to specify SSL version
6987
if isinstance(tls, TLSConfig):

docker/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import sys
2+
13
DEFAULT_DOCKER_API_VERSION = '1.22'
24
DEFAULT_TIMEOUT_SECONDS = 60
35
STREAM_HEADER_SIZE_BYTES = 8
@@ -8,3 +10,5 @@
810
INSECURE_REGISTRY_DEPRECATION_WARNING = \
911
'The `insecure_registry` argument to {} ' \
1012
'is deprecated and non-functional. Please remove it.'
13+
14+
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')

docker/transport/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# flake8: noqa
2+
from .unixconn import UnixAdapter
3+
try:
4+
from .npipeconn import NpipeAdapter
5+
except ImportError:
6+
pass

docker/transport/npipeconn.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import six
2+
import requests.adapters
3+
4+
from .npipesocket import NpipeSocket
5+
6+
if six.PY3:
7+
import http.client as httplib
8+
else:
9+
import httplib
10+
11+
try:
12+
import requests.packages.urllib3 as urllib3
13+
except ImportError:
14+
import urllib3
15+
16+
17+
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
18+
19+
20+
class NpipeHTTPConnection(httplib.HTTPConnection, object):
21+
def __init__(self, npipe_path, timeout=60):
22+
super(NpipeHTTPConnection, self).__init__(
23+
'localhost', timeout=timeout
24+
)
25+
self.npipe_path = npipe_path
26+
self.timeout = timeout
27+
28+
def connect(self):
29+
sock = NpipeSocket()
30+
sock.settimeout(self.timeout)
31+
sock.connect(self.npipe_path)
32+
self.sock = sock
33+
34+
35+
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
36+
def __init__(self, npipe_path, timeout=60):
37+
super(NpipeHTTPConnectionPool, self).__init__(
38+
'localhost', timeout=timeout
39+
)
40+
self.npipe_path = npipe_path
41+
self.timeout = timeout
42+
43+
def _new_conn(self):
44+
return NpipeHTTPConnection(
45+
self.npipe_path, self.timeout
46+
)
47+
48+
49+
class NpipeAdapter(requests.adapters.HTTPAdapter):
50+
def __init__(self, base_url, timeout=60):
51+
self.npipe_path = base_url.replace('npipe://', '')
52+
self.timeout = timeout
53+
self.pools = RecentlyUsedContainer(
54+
10, dispose_func=lambda p: p.close()
55+
)
56+
super(NpipeAdapter, self).__init__()
57+
58+
def get_connection(self, url, proxies=None):
59+
with self.pools.lock:
60+
pool = self.pools.get(url)
61+
if pool:
62+
return pool
63+
64+
pool = NpipeHTTPConnectionPool(
65+
self.npipe_path, self.timeout
66+
)
67+
self.pools[url] = pool
68+
69+
return pool
70+
71+
def request_url(self, request, proxies):
72+
# The select_proxy utility in requests errors out when the provided URL
73+
# doesn't have a hostname, like is the case when using a UNIX socket.
74+
# Since proxies are an irrelevant notion in the case of UNIX sockets
75+
# anyway, we simply return the path URL directly.
76+
# See also: https://github.com/docker/docker-py/issues/811
77+
return request.path_url
78+
79+
def close(self):
80+
self.pools.clear()

docker/transport/npipesocket.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import functools
2+
import io
3+
4+
import win32file
5+
import win32pipe
6+
7+
cSECURITY_SQOS_PRESENT = 0x100000
8+
cSECURITY_ANONYMOUS = 0
9+
cPIPE_READMODE_MESSAGE = 2
10+
11+
12+
def check_closed(f):
13+
@functools.wraps(f)
14+
def wrapped(self, *args, **kwargs):
15+
if self._closed:
16+
raise RuntimeError(
17+
'Can not reuse socket after connection was closed.'
18+
)
19+
return f(self, *args, **kwargs)
20+
return wrapped
21+
22+
23+
class NpipeSocket(object):
24+
""" Partial implementation of the socket API over windows named pipes.
25+
This implementation is only designed to be used as a client socket,
26+
and server-specific methods (bind, listen, accept...) are not
27+
implemented.
28+
"""
29+
def __init__(self, handle=None):
30+
self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT
31+
self._handle = handle
32+
self._closed = False
33+
34+
def accept(self):
35+
raise NotImplementedError()
36+
37+
def bind(self, address):
38+
raise NotImplementedError()
39+
40+
def close(self):
41+
self._handle.Close()
42+
self._closed = True
43+
44+
@check_closed
45+
def connect(self, address):
46+
win32pipe.WaitNamedPipe(address, self._timeout)
47+
handle = win32file.CreateFile(
48+
address,
49+
win32file.GENERIC_READ | win32file.GENERIC_WRITE,
50+
0,
51+
None,
52+
win32file.OPEN_EXISTING,
53+
cSECURITY_ANONYMOUS | cSECURITY_SQOS_PRESENT,
54+
0
55+
)
56+
self.flags = win32pipe.GetNamedPipeInfo(handle)[0]
57+
58+
self._handle = handle
59+
self._address = address
60+
61+
@check_closed
62+
def connect_ex(self, address):
63+
return self.connect(address)
64+
65+
@check_closed
66+
def detach(self):
67+
self._closed = True
68+
return self._handle
69+
70+
@check_closed
71+
def dup(self):
72+
return NpipeSocket(self._handle)
73+
74+
@check_closed
75+
def fileno(self):
76+
return int(self._handle)
77+
78+
def getpeername(self):
79+
return self._address
80+
81+
def getsockname(self):
82+
return self._address
83+
84+
def getsockopt(self, level, optname, buflen=None):
85+
raise NotImplementedError()
86+
87+
def ioctl(self, control, option):
88+
raise NotImplementedError()
89+
90+
def listen(self, backlog):
91+
raise NotImplementedError()
92+
93+
def makefile(self, mode=None, bufsize=None):
94+
if mode.strip('b') != 'r':
95+
raise NotImplementedError()
96+
rawio = NpipeFileIOBase(self)
97+
if bufsize is None:
98+
bufsize = io.DEFAULT_BUFFER_SIZE
99+
return io.BufferedReader(rawio, buffer_size=bufsize)
100+
101+
@check_closed
102+
def recv(self, bufsize, flags=0):
103+
err, data = win32file.ReadFile(self._handle, bufsize)
104+
return data
105+
106+
@check_closed
107+
def recvfrom(self, bufsize, flags=0):
108+
data = self.recv(bufsize, flags)
109+
return (data, self._address)
110+
111+
@check_closed
112+
def recvfrom_into(self, buf, nbytes=0, flags=0):
113+
return self.recv_into(buf, nbytes, flags), self._address
114+
115+
@check_closed
116+
def recv_into(self, buf, nbytes=0):
117+
readbuf = buf
118+
if not isinstance(buf, memoryview):
119+
readbuf = memoryview(buf)
120+
121+
err, data = win32file.ReadFile(
122+
self._handle,
123+
readbuf[:nbytes] if nbytes else readbuf
124+
)
125+
return len(data)
126+
127+
@check_closed
128+
def send(self, string, flags=0):
129+
err, nbytes = win32file.WriteFile(self._handle, string)
130+
return nbytes
131+
132+
@check_closed
133+
def sendall(self, string, flags=0):
134+
return self.send(string, flags)
135+
136+
@check_closed
137+
def sendto(self, string, address):
138+
self.connect(address)
139+
return self.send(string)
140+
141+
def setblocking(self, flag):
142+
if flag:
143+
return self.settimeout(None)
144+
return self.settimeout(0)
145+
146+
def settimeout(self, value):
147+
if value is None:
148+
self._timeout = win32pipe.NMPWAIT_NOWAIT
149+
elif not isinstance(value, (float, int)) or value < 0:
150+
raise ValueError('Timeout value out of range')
151+
elif value == 0:
152+
self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT
153+
else:
154+
self._timeout = value
155+
156+
def gettimeout(self):
157+
return self._timeout
158+
159+
def setsockopt(self, level, optname, value):
160+
raise NotImplementedError()
161+
162+
@check_closed
163+
def shutdown(self, how):
164+
return self.close()
165+
166+
167+
class NpipeFileIOBase(io.RawIOBase):
168+
def __init__(self, npipe_socket):
169+
self.sock = npipe_socket
170+
171+
def close(self):
172+
super(NpipeFileIOBase, self).close()
173+
self.sock = None
174+
175+
def fileno(self):
176+
return self.sock.fileno()
177+
178+
def isatty(self):
179+
return False
180+
181+
def readable(self):
182+
return True
183+
184+
def readinto(self, buf):
185+
return self.sock.recv_into(buf)
186+
187+
def seekable(self):
188+
return False
189+
190+
def writable(self):
191+
return False
File renamed without changes.

docker/unixconn/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

docker/utils/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,13 @@ def parse_repository_tag(repo_name):
383383
# fd:// protocol unsupported (for obvious reasons)
384384
# Added support for http and https
385385
# Protocol translation: tcp -> http, unix -> http+unix
386-
def parse_host(addr, platform=None, tls=False):
386+
def parse_host(addr, is_win32=False, tls=False):
387387
proto = "http+unix"
388388
host = DEFAULT_HTTP_HOST
389389
port = None
390390
path = ''
391391

392-
if not addr and platform == 'win32':
392+
if not addr and is_win32:
393393
addr = '{0}:{1}'.format(DEFAULT_HTTP_HOST, 2375)
394394

395395
if not addr or addr.strip() == 'unix://':
@@ -413,6 +413,9 @@ def parse_host(addr, platform=None, tls=False):
413413
elif addr.startswith('https://'):
414414
proto = "https"
415415
addr = addr[8:]
416+
elif addr.startswith('npipe://'):
417+
proto = 'npipe'
418+
addr = addr[8:]
416419
elif addr.startswith('fd://'):
417420
raise errors.DockerException("fd protocol is not implemented")
418421
else:
@@ -448,7 +451,7 @@ def parse_host(addr, platform=None, tls=False):
448451
else:
449452
host = addr
450453

451-
if proto == "http+unix":
454+
if proto == "http+unix" or proto == 'npipe':
452455
return "{0}://{1}".format(proto, host)
453456
return "{0}://{1}:{2}{3}".format(proto, host, port, path)
454457

0 commit comments

Comments
 (0)