|
@@ -1,10 +1,12 @@
|
|
|
import re
|
|
import re
|
|
|
|
|
+import struct
|
|
|
from hashlib import sha1
|
|
from hashlib import sha1
|
|
|
from threading import Thread
|
|
from threading import Thread
|
|
|
|
|
|
|
|
-from frame import receive_fragments
|
|
|
|
|
|
|
+from frame import ControlFrame, receive_fragments, receive_frame, \
|
|
|
|
|
+ OPCODE_CLOSE, OPCODE_PING, OPCODE_PONG
|
|
|
from message import create_message
|
|
from message import create_message
|
|
|
-from exceptions import SocketClosed
|
|
|
|
|
|
|
+from exceptions import SocketClosed, PingError
|
|
|
|
|
|
|
|
|
|
|
|
|
WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
|
|
@@ -17,6 +19,12 @@ class WebSocket(object):
|
|
|
self.address = address
|
|
self.address = address
|
|
|
self.encoding = encoding
|
|
self.encoding = encoding
|
|
|
|
|
|
|
|
|
|
+ self.received_close_params = None
|
|
|
|
|
+ self.close_frame_sent = False
|
|
|
|
|
+
|
|
|
|
|
+ self.ping_sent = False
|
|
|
|
|
+ self.ping_payload = None
|
|
|
|
|
+
|
|
|
def send_message(self, message, fragment_size=None):
|
|
def send_message(self, message, fragment_size=None):
|
|
|
if fragment_size is None:
|
|
if fragment_size is None:
|
|
|
self.send_frame(message.frame())
|
|
self.send_frame(message.frame())
|
|
@@ -26,8 +34,27 @@ class WebSocket(object):
|
|
|
def send_frame(self, frame):
|
|
def send_frame(self, frame):
|
|
|
self.sock.sendall(frame.pack())
|
|
self.sock.sendall(frame.pack())
|
|
|
|
|
|
|
|
|
|
+ def handle_control_frame(self, frame):
|
|
|
|
|
+ if frame.opcode == OPCODE_CLOSE:
|
|
|
|
|
+ self.received_close_params = frame.unpack_close()
|
|
|
|
|
+ elif frame.opcode == OPCODE_PING:
|
|
|
|
|
+ # Respond with a pong message with identical payload
|
|
|
|
|
+ self.send_frame(ControlFrame(OPCODE_PONG, frame.payload))
|
|
|
|
|
+ elif frame.opcode == OPCODE_PONG:
|
|
|
|
|
+ # Assert that the PONG payload is identical to that of the PING
|
|
|
|
|
+ if not self.ping_sent:
|
|
|
|
|
+ raise PingError('received PONG while no PING was sent')
|
|
|
|
|
+
|
|
|
|
|
+ self.ping_sent = False
|
|
|
|
|
+
|
|
|
|
|
+ if frame.payload != self.ping_payload:
|
|
|
|
|
+ raise PingError('received PONG with invalid payload')
|
|
|
|
|
+
|
|
|
|
|
+ self.ping_payload = None
|
|
|
|
|
+ self.onpong(frame.payload)
|
|
|
|
|
+
|
|
|
def receive_message(self):
|
|
def receive_message(self):
|
|
|
- frames = receive_fragments(self.sock)
|
|
|
|
|
|
|
+ frames = receive_fragments(self.sock, self.handle_control_frame)
|
|
|
payload = ''.join([f.payload for f in frames])
|
|
payload = ''.join([f.payload for f in frames])
|
|
|
return create_message(frames[0].opcode, payload)
|
|
return create_message(frames[0].opcode, payload)
|
|
|
|
|
|
|
@@ -79,14 +106,60 @@ class WebSocket(object):
|
|
|
try:
|
|
try:
|
|
|
while True:
|
|
while True:
|
|
|
self.onmessage(self, self.receive_message())
|
|
self.onmessage(self, self.receive_message())
|
|
|
|
|
+
|
|
|
|
|
+ if self.received_close_params is not None:
|
|
|
|
|
+ self.handle_close(*self.received_close_params)
|
|
|
|
|
+ break
|
|
|
except SocketClosed:
|
|
except SocketClosed:
|
|
|
- self.onclose()
|
|
|
|
|
|
|
+ self.onclose(None, '')
|
|
|
|
|
|
|
|
def run_threaded(self, daemon=True):
|
|
def run_threaded(self, daemon=True):
|
|
|
t = Thread(target=self.receive_forever)
|
|
t = Thread(target=self.receive_forever)
|
|
|
t.daemon = daemon
|
|
t.daemon = daemon
|
|
|
t.start()
|
|
t.start()
|
|
|
|
|
|
|
|
|
|
+ def send_close(self, code, reason):
|
|
|
|
|
+ payload = '' if code is None else struct.pack('!H', code)
|
|
|
|
|
+ self.send_frame(ControlFrame(OPCODE_CLOSE, payload))
|
|
|
|
|
+ self.close_frame_sent = True
|
|
|
|
|
+
|
|
|
|
|
+ def send_ping(self, payload=''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Send a ping control frame with an optional payload.
|
|
|
|
|
+ """
|
|
|
|
|
+ self.send_frame(ControlFrame(OPCODE_PING, payload))
|
|
|
|
|
+ self.ping_payload = payload
|
|
|
|
|
+ self.ping_sent = True
|
|
|
|
|
+ self.onping()
|
|
|
|
|
+
|
|
|
|
|
+ def handle_close(self, code=None, reason=''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Handle a close message by sending a response close message if no close
|
|
|
|
|
+ message was sent before, and closing the connection. The onclose()
|
|
|
|
|
+ handler is called afterwards.
|
|
|
|
|
+ """
|
|
|
|
|
+ if not self.close_frame_sent:
|
|
|
|
|
+ payload = '' if code is None else struct.pack('!H', code)
|
|
|
|
|
+ self.send_frame(ControlFrame(OPCODE_CLOSE, payload))
|
|
|
|
|
+
|
|
|
|
|
+ self.sock.close()
|
|
|
|
|
+ self.onclose(code, reason)
|
|
|
|
|
+
|
|
|
|
|
+ def close(self, code=None, reason=''):
|
|
|
|
|
+ """
|
|
|
|
|
+ Close the socket by sending a close message and waiting for a response
|
|
|
|
|
+ close message. The onclose() handler is called after the close message
|
|
|
|
|
+ has been sent, but before the response has been received.
|
|
|
|
|
+ """
|
|
|
|
|
+ self.send_close(code, reason)
|
|
|
|
|
+ # FIXME: swap the two lines below?
|
|
|
|
|
+ self.onclose(code, reason)
|
|
|
|
|
+ frame = receive_frame(self.sock)
|
|
|
|
|
+ self.sock.close()
|
|
|
|
|
+
|
|
|
|
|
+ if frame.opcode != OPCODE_CLOSE:
|
|
|
|
|
+ raise ValueError('Expected close frame, got %s instead' % frame)
|
|
|
|
|
+
|
|
|
def onopen(self):
|
|
def onopen(self):
|
|
|
"""
|
|
"""
|
|
|
Called after the handshake has completed.
|
|
Called after the handshake has completed.
|
|
@@ -100,11 +173,22 @@ class WebSocket(object):
|
|
|
"""
|
|
"""
|
|
|
raise NotImplemented
|
|
raise NotImplemented
|
|
|
|
|
|
|
|
- def onclose(self):
|
|
|
|
|
|
|
+ def onping(self, payload):
|
|
|
"""
|
|
"""
|
|
|
- Called when the other end of the socket disconnects.
|
|
|
|
|
|
|
+ Called after a ping control frame has been sent. This handler could be
|
|
|
|
|
+ used to start a timeout handler for a pong message that is not received
|
|
|
|
|
+ in time.
|
|
|
"""
|
|
"""
|
|
|
- pass
|
|
|
|
|
|
|
+ raise NotImplemented
|
|
|
|
|
|
|
|
- def close(self):
|
|
|
|
|
- raise SocketClosed()
|
|
|
|
|
|
|
+ def onpong(self, payload):
|
|
|
|
|
+ """
|
|
|
|
|
+ Called when a pong control frame is received.
|
|
|
|
|
+ """
|
|
|
|
|
+ raise NotImplemented
|
|
|
|
|
+
|
|
|
|
|
+ def onclose(self, code, reason):
|
|
|
|
|
+ """
|
|
|
|
|
+ Called when the socket is closed by either end point.
|
|
|
|
|
+ """
|
|
|
|
|
+ pass
|