Skip to content

Commit 75b88bd

Browse files
committed
Fix #167 hopefully by reading the internal SSL socket object's buffer
1 parent 99a14c4 commit 75b88bd

File tree

4 files changed

+129
-3
lines changed

4 files changed

+129
-3
lines changed

example/bug167_client.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
def run_threaded():
2+
from ws4py.client.threadedclient import WebSocketClient
3+
class EchoClient(WebSocketClient):
4+
def opened(self):
5+
self.send("hello")
6+
7+
def closed(self, code, reason=None):
8+
print(("Closed down", code, reason))
9+
10+
def received_message(self, m):
11+
print(m)
12+
self.close()
13+
14+
try:
15+
ws = EchoClient('wss://localhost:9000/ws')
16+
ws.connect()
17+
ws.run_forever()
18+
except KeyboardInterrupt:
19+
ws.close()
20+
21+
def run_tornado():
22+
from tornado import ioloop
23+
from ws4py.client.tornadoclient import TornadoWebSocketClient
24+
class MyClient(TornadoWebSocketClient):
25+
def opened(self):
26+
self.send("hello")
27+
28+
def closed(self, code, reason=None):
29+
print(("Closed down", code, reason))
30+
ioloop.IOLoop.instance().stop()
31+
32+
def received_message(self, m):
33+
print(m)
34+
self.close()
35+
36+
ws = MyClient('wss://localhost:9000/ws')
37+
ws.connect()
38+
39+
ioloop.IOLoop.instance().start()
40+
41+
def run_gevent():
42+
from gevent import monkey; monkey.patch_all()
43+
import gevent
44+
from ws4py.client.geventclient import WebSocketClient
45+
46+
ws = WebSocketClient('wss://localhost:9000/ws')
47+
ws.connect()
48+
49+
ws.send("hello")
50+
51+
def incoming():
52+
while True:
53+
m = ws.receive()
54+
if m is not None:
55+
print(m)
56+
else:
57+
break
58+
59+
ws.close()
60+
61+
gevent.joinall([gevent.spawn(incoming)])
62+
63+
#run_gevent()
64+
run_threaded()
65+
run_tornado()

ws4py/client/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,8 @@ def connect(self):
205205
if self.scheme == "wss":
206206
# default port is now 443; upgrade self.sender to send ssl
207207
self.sock = ssl.wrap_socket(self.sock, **self.ssl_options)
208-
208+
self._is_secure = True
209+
209210
self.sock.connect(self.bind_addr)
210211

211212
self._write(self.handshake_request)

ws4py/client/tornadoclient.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def closed(self, code, reason=None):
3535
ssl_options=ssl_options, headers=headers)
3636
if self.scheme == "wss":
3737
self.sock = ssl.wrap_socket(self.sock, do_handshake_on_connect=False, **self.ssl_options)
38+
self._is_secure = True
3839
self.io = iostream.SSLIOStream(self.sock, io_loop, ssl_options=self.ssl_options)
3940
else:
4041
self.io = iostream.IOStream(self.sock, io_loop)

ws4py/websocket.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# -*- coding: utf-8 -*-
22
import logging
33
import socket
4+
import ssl
45
import time
56
import threading
67
import types
78

9+
try:
10+
from OpenSSL.SSL import Error as pyOpenSSLError
11+
except ImportError:
12+
class pyOpenSSLError(Exception):
13+
pass
14+
815
from ws4py import WS_KEY, WS_VERSION
916
from ws4py.exc import HandshakeError, StreamClosed
1017
from ws4py.streaming import Stream
@@ -99,7 +106,12 @@ def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbea
99106
"""
100107
Underlying connection.
101108
"""
102-
109+
110+
self._is_secure = hasattr(sock, '_ssl') or hasattr(sock, '_sslobj')
111+
"""
112+
Tell us if the socket is secure or not.
113+
"""
114+
103115
self.client_terminated = False
104116
"""
105117
Indicates if the client has been marked as terminated.
@@ -301,6 +313,50 @@ def send(self, payload, binary=False):
301313
else:
302314
raise ValueError("Unsupported type '%s' passed to send()" % type(payload))
303315

316+
def _get_from_pending(self):
317+
"""
318+
The SSL socket object provides the same interface
319+
as the socket interface but behaves differently.
320+
321+
When data is sent over a SSL connection
322+
more data may be read than was requested from by
323+
the ws4py websocket object.
324+
325+
In that case, the data may have been indeed read
326+
from the underlying real socket, but not read by the
327+
application which will expect another trigger from the
328+
manager's polling mechanism as if more data was still on the
329+
wire. This will happen only when new data is
330+
sent by the other peer which means there will be
331+
some delay before the initial read data is handled
332+
by the application.
333+
334+
Due to this, we have to rely on a non-public method
335+
to query the internal SSL socket buffer if it has indeed
336+
more data pending in its buffer.
337+
338+
Now, some people in the Python community
339+
`discourage <https://bugs.python.org/issue21430>`_
340+
this usage of the ``pending()`` method because it's not
341+
the right way of dealing with such use case. They advise
342+
`this approach <https://docs.python.org/dev/library/ssl.html#notes-on-non-blocking-sockets>`_
343+
instead. Unfortunately, this applies only if the
344+
application can directly control the poller which is not
345+
the case with the WebSocket abstraction here.
346+
347+
We therefore rely on this `technic <http://stackoverflow.com/questions/3187565/select-and-ssl-in-python>`_
348+
which seems to be valid anyway.
349+
350+
This is a bit of a shame because we have to process
351+
more data than what wanted initially.
352+
"""
353+
data = b""
354+
pending = self.sock.pending()
355+
while pending:
356+
data += self.sock.recv(pending)
357+
pending = self.sock.pending()
358+
return data
359+
304360
def once(self):
305361
"""
306362
Performs the operation of reading from the underlying
@@ -322,7 +378,10 @@ def once(self):
322378

323379
try:
324380
b = self.sock.recv(self.reading_buffer_size)
325-
except (socket.error, OSError) as e:
381+
# This will only make sense with secure sockets.
382+
if self._is_secure:
383+
b += self._get_from_pending()
384+
except (socket.error, OSError, pyOpenSSLError) as e:
326385
self.unhandled_error(e)
327386
return False
328387
else:

0 commit comments

Comments
 (0)