|
@@ -1,7 +1,7 @@
|
|
|
import socket
|
|
import socket
|
|
|
import ssl
|
|
import ssl
|
|
|
|
|
|
|
|
-from frame import receive_frame
|
|
|
|
|
|
|
+from frame import receive_frame, pop_frame, contains_frame
|
|
|
from handshake import ServerHandshake, ClientHandshake
|
|
from handshake import ServerHandshake, ClientHandshake
|
|
|
from errors import SSLError
|
|
from errors import SSLError
|
|
|
|
|
|
|
@@ -11,7 +11,6 @@ INHERITED_ATTRS = ['bind', 'close', 'listen', 'fileno', 'getpeername',
|
|
|
'settimeout', 'gettimeout', 'shutdown', 'family', 'type',
|
|
'settimeout', 'gettimeout', 'shutdown', 'family', 'type',
|
|
|
'proto']
|
|
'proto']
|
|
|
|
|
|
|
|
-
|
|
|
|
|
class websocket(object):
|
|
class websocket(object):
|
|
|
"""
|
|
"""
|
|
|
Implementation of web socket, upgrades a regular TCP socket to a websocket
|
|
Implementation of web socket, upgrades a regular TCP socket to a websocket
|
|
@@ -36,22 +35,23 @@ class websocket(object):
|
|
|
>>> sock.connect(('', 8000))
|
|
>>> sock.connect(('', 8000))
|
|
|
>>> sock.send(wspy.Frame(wspy.OPCODE_TEXT, 'Hello, Server!'))
|
|
>>> sock.send(wspy.Frame(wspy.OPCODE_TEXT, 'Hello, Server!'))
|
|
|
"""
|
|
"""
|
|
|
- def __init__(self, sock=None, protocols=[], extensions=[], origin=None,
|
|
|
|
|
|
|
+ def __init__(self, sock=None, origin=None, protocols=[], extensions=[],
|
|
|
location='/', trusted_origins=[], locations=[], auth=None,
|
|
location='/', trusted_origins=[], locations=[], auth=None,
|
|
|
- sfamily=socket.AF_INET, sproto=0):
|
|
|
|
|
|
|
+ recv_callback=None, sfamily=socket.AF_INET, sproto=0):
|
|
|
"""
|
|
"""
|
|
|
Create a regular TCP socket of family `family` and protocol
|
|
Create a regular TCP socket of family `family` and protocol
|
|
|
|
|
|
|
|
`sock` is an optional regular TCP socket to be used for sending binary
|
|
`sock` is an optional regular TCP socket to be used for sending binary
|
|
|
data. If not specified, a new socket is created.
|
|
data. If not specified, a new socket is created.
|
|
|
|
|
|
|
|
- `protocols` is a list of supported protocol names.
|
|
|
|
|
-
|
|
|
|
|
- `extensions` is a list of supported extensions (`Extension` instances).
|
|
|
|
|
-
|
|
|
|
|
`origin` (for client sockets) is the value for the "Origin" header sent
|
|
`origin` (for client sockets) is the value for the "Origin" header sent
|
|
|
in a client handshake .
|
|
in a client handshake .
|
|
|
|
|
|
|
|
|
|
+ `protocols` is a list of supported protocol names.
|
|
|
|
|
+
|
|
|
|
|
+ `extensions` (for server sockets) is a list of supported extensions
|
|
|
|
|
+ (`Extension` instances).
|
|
|
|
|
+
|
|
|
`location` (for client sockets) is optional, used to request a
|
|
`location` (for client sockets) is optional, used to request a
|
|
|
particular resource in the HTTP handshake. In a URL, this would show as
|
|
particular resource in the HTTP handshake. In a URL, this would show as
|
|
|
ws://host[:port]/<location>. Use this when the server serves multiple
|
|
ws://host[:port]/<location>. Use this when the server serves multiple
|
|
@@ -71,10 +71,17 @@ class websocket(object):
|
|
|
`auth` is optional, used for HTTP Basic or Digest authentication during
|
|
`auth` is optional, used for HTTP Basic or Digest authentication during
|
|
|
the handshake. It must be specified as a (username, password) tuple.
|
|
the handshake. It must be specified as a (username, password) tuple.
|
|
|
|
|
|
|
|
|
|
+ `recv_callback` is the callback for received frames in asynchronous
|
|
|
|
|
+ sockets. Use in conjunction with setblocking(0). The callback itself
|
|
|
|
|
+ may for example change the recv_callback attribute to change the
|
|
|
|
|
+ behaviour for the next received message. Can be set when calling
|
|
|
|
|
+ `queue_send`.
|
|
|
|
|
+
|
|
|
`sfamily` and `sproto` are used for the regular socket constructor.
|
|
`sfamily` and `sproto` are used for the regular socket constructor.
|
|
|
"""
|
|
"""
|
|
|
self.protocols = protocols
|
|
self.protocols = protocols
|
|
|
self.extensions = extensions
|
|
self.extensions = extensions
|
|
|
|
|
+ self.extension_hooks = []
|
|
|
self.origin = origin
|
|
self.origin = origin
|
|
|
self.location = location
|
|
self.location = location
|
|
|
self.trusted_origins = trusted_origins
|
|
self.trusted_origins = trusted_origins
|
|
@@ -85,11 +92,16 @@ class websocket(object):
|
|
|
|
|
|
|
|
self.handshake_sent = False
|
|
self.handshake_sent = False
|
|
|
|
|
|
|
|
- self.hooks_send = []
|
|
|
|
|
- self.hooks_recv = []
|
|
|
|
|
|
|
+ self.sendbuf_frames = []
|
|
|
|
|
+ self.sendbuf = ''
|
|
|
|
|
+ self.recvbuf = ''
|
|
|
|
|
+ self.recv_callback = recv_callback
|
|
|
|
|
|
|
|
self.sock = sock or socket.socket(sfamily, socket.SOCK_STREAM, sproto)
|
|
self.sock = sock or socket.socket(sfamily, socket.SOCK_STREAM, sproto)
|
|
|
|
|
|
|
|
|
|
+ def set_extensions(self, extensions):
|
|
|
|
|
+ self.extensions = [ext.Hook() for ext in extensions]
|
|
|
|
|
+
|
|
|
def __getattr__(self, name):
|
|
def __getattr__(self, name):
|
|
|
if name in INHERITED_ATTRS:
|
|
if name in INHERITED_ATTRS:
|
|
|
return getattr(self.sock, name)
|
|
return getattr(self.sock, name)
|
|
@@ -122,29 +134,31 @@ class websocket(object):
|
|
|
ClientHandshake(self).perform()
|
|
ClientHandshake(self).perform()
|
|
|
self.handshake_sent = True
|
|
self.handshake_sent = True
|
|
|
|
|
|
|
|
|
|
+ def apply_send_hooks(self, frame):
|
|
|
|
|
+ for hook in self.extension_hooks:
|
|
|
|
|
+ frame = hook.send(frame)
|
|
|
|
|
+
|
|
|
|
|
+ return frame
|
|
|
|
|
+
|
|
|
|
|
+ def apply_recv_hooks(self, frame):
|
|
|
|
|
+ for hook in reversed(self.extension_hooks):
|
|
|
|
|
+ frame = hook.recv(frame)
|
|
|
|
|
+
|
|
|
|
|
+ return frame
|
|
|
|
|
+
|
|
|
def send(self, *args):
|
|
def send(self, *args):
|
|
|
"""
|
|
"""
|
|
|
Send a number of frames.
|
|
Send a number of frames.
|
|
|
"""
|
|
"""
|
|
|
for frame in args:
|
|
for frame in args:
|
|
|
- for hook in self.hooks_send:
|
|
|
|
|
- frame = hook(frame)
|
|
|
|
|
-
|
|
|
|
|
- #print 'send frame:', frame, 'to %s:%d' % self.sock.getpeername()
|
|
|
|
|
- self.sock.sendall(frame.pack())
|
|
|
|
|
|
|
+ self.sock.sendall(self.apply_send_hooks(frame).pack())
|
|
|
|
|
|
|
|
def recv(self):
|
|
def recv(self):
|
|
|
"""
|
|
"""
|
|
|
Receive a single frames. This can be either a data frame or a control
|
|
Receive a single frames. This can be either a data frame or a control
|
|
|
frame.
|
|
frame.
|
|
|
"""
|
|
"""
|
|
|
- frame = receive_frame(self.sock)
|
|
|
|
|
-
|
|
|
|
|
- for hook in self.hooks_recv:
|
|
|
|
|
- frame = hook(frame)
|
|
|
|
|
-
|
|
|
|
|
- #print 'receive frame:', frame, 'from %s:%d' % self.sock.getpeername()
|
|
|
|
|
- return frame
|
|
|
|
|
|
|
+ return self.apply_recv_hooks(receive_frame(self.sock))
|
|
|
|
|
|
|
|
def recvn(self, n):
|
|
def recvn(self, n):
|
|
|
"""
|
|
"""
|
|
@@ -153,47 +167,79 @@ class websocket(object):
|
|
|
"""
|
|
"""
|
|
|
return [self.recv() for i in xrange(n)]
|
|
return [self.recv() for i in xrange(n)]
|
|
|
|
|
|
|
|
- def enable_ssl(self, *args, **kwargs):
|
|
|
|
|
|
|
+ def queue_send(self, frame, callback=None, recv_callback=None):
|
|
|
"""
|
|
"""
|
|
|
- Transforms the regular socket.socket to an ssl.SSLSocket for secure
|
|
|
|
|
- connections. Any arguments are passed to ssl.wrap_socket:
|
|
|
|
|
- http://docs.python.org/dev/library/ssl.html#ssl.wrap_socket
|
|
|
|
|
|
|
+ Enqueue `frame` to the send buffer so that it is send on the next
|
|
|
|
|
+ `do_async_send`. `callback` is an optional callable to call when the
|
|
|
|
|
+ frame has been fully written. `recv_callback` is an optional callable
|
|
|
|
|
+ to quickly set the `recv_callback` attribute to.
|
|
|
"""
|
|
"""
|
|
|
- if self.handshake_sent:
|
|
|
|
|
- raise SSLError('can only enable SSL before handshake')
|
|
|
|
|
|
|
+ frame = self.apply_send_hooks(frame)
|
|
|
|
|
+ self.sendbuf += frame.pack()
|
|
|
|
|
+ self.sendbuf_frames.append([frame, len(self.sendbuf), callback])
|
|
|
|
|
|
|
|
- self.secure = True
|
|
|
|
|
- self.sock = ssl.wrap_socket(self.sock, *args, **kwargs)
|
|
|
|
|
|
|
+ if recv_callback:
|
|
|
|
|
+ self.recv_callback = recv_callback
|
|
|
|
|
+
|
|
|
|
|
+ def do_async_send(self):
|
|
|
|
|
+ """
|
|
|
|
|
+ Send any queued data. This function should only be called after a write
|
|
|
|
|
+ event on a file descriptor.
|
|
|
|
|
+ """
|
|
|
|
|
+ assert len(self.sendbuf)
|
|
|
|
|
+
|
|
|
|
|
+ nwritten = self.sock.send(self.sendbuf)
|
|
|
|
|
+ nframes = 0
|
|
|
|
|
|
|
|
- def add_hook(self, send=None, recv=None, prepend=False):
|
|
|
|
|
|
|
+ for entry in self.sendbuf_frames:
|
|
|
|
|
+ frame, offset, callback = entry
|
|
|
|
|
+
|
|
|
|
|
+ if offset <= nwritten:
|
|
|
|
|
+ nframes += 1
|
|
|
|
|
+
|
|
|
|
|
+ if callback:
|
|
|
|
|
+ callback()
|
|
|
|
|
+ else:
|
|
|
|
|
+ entry[1] -= nwritten
|
|
|
|
|
+
|
|
|
|
|
+ self.sendbuf = self.sendbuf[nwritten:]
|
|
|
|
|
+ self.sendbuf_frames = self.sendbuf_frames[nframes:]
|
|
|
|
|
+
|
|
|
|
|
+ def do_async_recv(self, bufsize):
|
|
|
|
|
+ """
|
|
|
|
|
+ Receive any completed frames from the socket. This function should only
|
|
|
|
|
+ be called after a read event on a file descriptor.
|
|
|
"""
|
|
"""
|
|
|
- Add a pair of send and receive hooks that are called for each frame
|
|
|
|
|
- that is sent or received. A hook is a function that receives a single
|
|
|
|
|
- argument - a Frame instance - and returns a `Frame` instance as well.
|
|
|
|
|
|
|
+ data = self.sock.recv(bufsize)
|
|
|
|
|
+
|
|
|
|
|
+ if len(data) == 0:
|
|
|
|
|
+ raise socket.error('no data to receive')
|
|
|
|
|
|
|
|
- `prepend` is a flag indicating whether the send hook is prepended to
|
|
|
|
|
- the other send hooks. This is expecially useful when a program uses
|
|
|
|
|
- extensions such as the built-in `DeflateFrame` extension. These
|
|
|
|
|
- extensions are installed using these hooks as well.
|
|
|
|
|
|
|
+ self.recvbuf += data
|
|
|
|
|
|
|
|
- For example, the following code creates a `Frame` instance for data
|
|
|
|
|
- being sent and removes the instance for received data. This way, data
|
|
|
|
|
- can be sent and received as if on a regular socket.
|
|
|
|
|
- >>> import wspy
|
|
|
|
|
- >>> sock.add_hook(lambda data: tswpy.Frame(tswpy.OPCODE_TEXT, data),
|
|
|
|
|
- >>> lambda frame: frame.payload)
|
|
|
|
|
|
|
+ while contains_frame(self.recvbuf):
|
|
|
|
|
+ frame, self.recvbuf = pop_frame(self.recvbuf)
|
|
|
|
|
+ frame = self.apply_recv_hooks(frame)
|
|
|
|
|
|
|
|
- To add base64 encoding to the example above:
|
|
|
|
|
- >>> import base64
|
|
|
|
|
- >>> sock.add_hook(base64.encodestring, base64.decodestring, True)
|
|
|
|
|
|
|
+ if not self.recv_callback:
|
|
|
|
|
+ raise ValueError('no callback installed for %s' % frame)
|
|
|
|
|
|
|
|
- Note that here `prepend=True`, so that data passed to `send()` is first
|
|
|
|
|
- encoded and then packed into a frame. Of course, one could also decide
|
|
|
|
|
- to add the base64 hook first, or to return a new `Frame` instance with
|
|
|
|
|
- base64-encoded data.
|
|
|
|
|
|
|
+ self.recv_callback(frame)
|
|
|
|
|
+
|
|
|
|
|
+ def can_send(self):
|
|
|
|
|
+ return len(self.sendbuf) > 0
|
|
|
|
|
+
|
|
|
|
|
+ def can_recv(self):
|
|
|
|
|
+ return self.recv_callback is not None
|
|
|
|
|
+
|
|
|
|
|
+ def enable_ssl(self, *args, **kwargs):
|
|
|
|
|
+ """
|
|
|
|
|
+ Transforms the regular socket.socket to an ssl.SSLSocket for secure
|
|
|
|
|
+ connections. Any arguments are passed to ssl.wrap_socket:
|
|
|
|
|
+ http://docs.python.org/dev/library/ssl.html#ssl.wrap_socket
|
|
|
"""
|
|
"""
|
|
|
- if send:
|
|
|
|
|
- self.hooks_send.insert(0 if prepend else -1, send)
|
|
|
|
|
|
|
+ if self.handshake_sent:
|
|
|
|
|
+ raise SSLError('can only enable SSL before handshake')
|
|
|
|
|
|
|
|
- if recv:
|
|
|
|
|
- self.hooks_recv.insert(-1 if prepend else 0, recv)
|
|
|
|
|
|
|
+ self.secure = True
|
|
|
|
|
+ self.sock = ssl.wrap_socket(self.sock, *args, **kwargs)
|