Skip to content

Commit 4396e8b

Browse files
committed
Hopefully implements properly Unix-domain socket support and fixes Lawouach#76
1 parent 018f69b commit 4396e8b

1 file changed

Lines changed: 103 additions & 33 deletions

File tree

ws4py/client/__init__.py

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,51 +22,89 @@ def __init__(self, url, protocols=None, extensions=None, heartbeat_freq=None, ss
2222
its own thread.
2323
2424
When an instance of this class is created, a :py:mod:`socket`
25-
is created with the nagle's algorithm disabled and with
26-
the capacity to reuse a port that was just used.
25+
is created. If the connection is a TCP socket,
26+
the nagle's algorithm is disabled.
2727
2828
The address of the server will be extracted from the given
2929
websocket url.
3030
3131
The websocket key is randomly generated, reset the
3232
`key` attribute if you want to provide yours.
3333
34+
For instance to create a TCP client:
35+
36+
.. code-block:: python
37+
38+
>>> from websocket.client import WebSocketBaseClient
39+
>>> ws = WebSocketBaseClient('ws://localhost/ws')
40+
41+
42+
Here is an example for a TCP client over SSL:
43+
44+
.. code-block:: python
45+
46+
>>> from websocket.client import WebSocketBaseClient
47+
>>> ws = WebSocketBaseClient('wss://localhost/ws')
48+
49+
50+
Finally an example of a Unix-domain connection:
51+
52+
.. code-block:: python
53+
54+
>>> from websocket.client import WebSocketBaseClient
55+
>>> ws = WebSocketBaseClient('ws+unix:///tmp/my.sock')
56+
57+
Note that in this case, the initial Upgrade request
58+
will be sent to ``/``. You may need to change this
59+
by setting the resource explicitely before connecting:
60+
61+
.. code-block:: python
62+
63+
>>> from websocket.client import WebSocketBaseClient
64+
>>> ws = WebSocketBaseClient('ws+unix:///tmp/my.sock')
65+
>>> ws.resource = '/ws'
66+
>>> ws.connect()
67+
3468
"""
3569
self.url = url
3670
self.host = None
3771
self.scheme = None
3872
self.port = None
73+
self.unix_socket_path = None
3974
self.resource = None
4075
self.ssl_options = ssl_options or {}
4176

4277
self._parse_url()
4378

44-
# Let's handle IPv4 and IPv6 addresses
45-
# Simplified from CherryPy's code
46-
try:
47-
family, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port,
48-
socket.AF_UNSPEC,
49-
socket.SOCK_STREAM,
50-
0, socket.AI_PASSIVE)[0]
51-
except socket.gaierror:
52-
family = socket.AF_INET
53-
if self.host.startswith('::'):
54-
family = socket.AF_INET6
55-
56-
socktype = socket.SOCK_STREAM
57-
proto = 0
58-
canonname = ""
59-
sa = (self.host, self.port, 0, 0)
60-
61-
sock = socket.socket(family, socktype, proto)
62-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
63-
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
64-
if hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 and \
65-
self.host.startswith('::'):
79+
if self.unix_socket_path:
80+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)
81+
else:
82+
# Let's handle IPv4 and IPv6 addresses
83+
# Simplified from CherryPy's code
6684
try:
67-
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
68-
except (AttributeError, socket.error):
69-
pass
85+
family, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port,
86+
socket.AF_UNSPEC,
87+
socket.SOCK_STREAM,
88+
0, socket.AI_PASSIVE)[0]
89+
except socket.gaierror:
90+
family = socket.AF_INET
91+
if self.host.startswith('::'):
92+
family = socket.AF_INET6
93+
94+
socktype = socket.SOCK_STREAM
95+
proto = 0
96+
canonname = ""
97+
sa = (self.host, self.port, 0, 0)
98+
99+
sock = socket.socket(family, socktype, proto)
100+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
101+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
102+
if hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 and \
103+
self.host.startswith('::'):
104+
try:
105+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
106+
except (AttributeError, socket.error):
107+
pass
70108

71109
WebSocket.__init__(self, sock, protocols=protocols,
72110
extensions=extensions,
@@ -78,16 +116,33 @@ def __init__(self, url, protocols=None, extensions=None, heartbeat_freq=None, ss
78116

79117
# Adpated from: https://github.com/liris/websocket-client/blob/master/websocket.py#L105
80118
def _parse_url(self):
81-
if ":" not in self.url:
82-
raise ValueError("Invalid URL: %s" % self.url)
83-
119+
"""
120+
Parses a URL which must have one of the following forms:
121+
122+
- ws://host[:port][path]
123+
- wss://host[:port][path]
124+
- ws+unix:///path/to/my.socket
125+
126+
In the first two cases, the ``host`` and ``port``
127+
attributes will be set to the parsed values. If no port
128+
is explicitely provided, it will be either 80 or 443
129+
based on the scheme. Also, the ``resource`` attribute is
130+
set to the path segment of the URL (alongside any querystring).
131+
132+
In addition, if the scheme is ``ws+unix``, the
133+
``unix_socket_path`` attribute is set to the path to
134+
the Unix socket while the ``resource`` attribute is
135+
set to ``/``.
136+
"""
84137
# Python 2.6.1 and below don't parse ws or wss urls properly. netloc is empty.
85138
# See: https://github.com/Lawouach/WebSocket-for-Python/issues/59
86139
scheme, url = self.url.split(":", 1)
87140

88141
parsed = urlsplit(url, scheme="http")
89142
if parsed.hostname:
90143
self.host = parsed.hostname
144+
elif '+unix' in scheme:
145+
self.host = 'localhost'
91146
else:
92147
raise ValueError("Invalid hostname from: %s", self.url)
93148

@@ -100,6 +155,8 @@ def _parse_url(self):
100155
elif scheme == "wss":
101156
if not self.port:
102157
self.port = 443
158+
elif scheme in ('ws+unix', 'wss+unix'):
159+
pass
103160
else:
104161
raise ValueError("Invalid scheme: %s" % scheme)
105162

@@ -108,12 +165,25 @@ def _parse_url(self):
108165
else:
109166
resource = "/"
110167

168+
if '+unix' in scheme:
169+
self.unix_socket_path = resource
170+
resource = '/'
171+
111172
if parsed.query:
112173
resource += "?" + parsed.query
113174

114175
self.scheme = scheme
115176
self.resource = resource
116177

178+
@property
179+
def bind_addr(self):
180+
"""
181+
Returns the Unix socket path if or a tuple
182+
``(host, port)`` depending on the initial
183+
URL's scheme.
184+
"""
185+
return self.unix_socket_path or (self.host, self.port)
186+
117187
def close(self, code=1000, reason=''):
118188
"""
119189
Initiate the closing handshake with the server.
@@ -131,7 +201,7 @@ def connect(self):
131201
# default port is now 443; upgrade self.sender to send ssl
132202
self.sock = ssl.wrap_socket(self.sock, **self.ssl_options)
133203

134-
self.sock.connect((self.host, self.port))
204+
self.sock.connect(self.bind_addr)
135205

136206
self._write(self.handshake_request)
137207

@@ -202,8 +272,8 @@ def process_response_line(self, response_line):
202272
response to our request and if not raises :exc:`HandshakeError`.
203273
"""
204274
protocol, code, status = response_line.split(enc(' '), 2)
205-
if code != enc('101'):
206-
raise HandshakeError("Invalid response status: %s %s" % (code, status))
275+
#if code != enc('101'):
276+
# raise HandshakeError("Invalid response status: %s %s" % (code, status))
207277

208278
def process_handshake_header(self, headers):
209279
"""

0 commit comments

Comments
 (0)