2011-03-26 20:27:08 +00:00
|
|
|
#!/usr/bin/env python
|
2010-05-06 16:32:07 +01:00
|
|
|
|
|
|
|
'''
|
2016-09-15 18:51:26 +01:00
|
|
|
Python WebSocket library
|
2011-05-18 17:09:10 +01:00
|
|
|
Copyright 2011 Joel Martin
|
2016-09-15 18:51:26 +01:00
|
|
|
Copyright 2016 Pierre Ossman
|
2010-07-17 18:05:58 +01:00
|
|
|
Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
|
2010-05-06 16:32:07 +01:00
|
|
|
|
2011-05-02 04:17:04 +01:00
|
|
|
Supports following protocol versions:
|
2013-03-04 08:38:29 +00:00
|
|
|
- http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
|
2011-08-04 17:09:12 +01:00
|
|
|
- http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
2013-03-04 08:38:29 +00:00
|
|
|
- http://tools.ietf.org/html/rfc6455
|
2010-05-06 16:32:07 +01:00
|
|
|
'''
|
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
import sys
|
|
|
|
import array
|
|
|
|
import email
|
|
|
|
import errno
|
|
|
|
import random
|
|
|
|
import socket
|
|
|
|
import ssl
|
|
|
|
import struct
|
2017-01-28 13:50:48 +00:00
|
|
|
from base64 import b64encode
|
2017-01-28 13:46:33 +00:00
|
|
|
from hashlib import sha1
|
2011-05-18 17:09:10 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
try:
|
|
|
|
import numpy
|
|
|
|
except ImportError:
|
|
|
|
import warnings
|
|
|
|
warnings.warn("no 'numpy' module, HyBi protocol will be slower")
|
|
|
|
numpy = None
|
2011-10-06 15:37:09 +01:00
|
|
|
|
|
|
|
# python 3.0 differences
|
2016-09-15 18:51:26 +01:00
|
|
|
try: from urllib.parse import urlparse
|
|
|
|
except: from urlparse import urlparse
|
|
|
|
|
|
|
|
# SSLWant*Error is 2.7.9+
|
|
|
|
try:
|
|
|
|
class WebSocketWantReadError(ssl.SSLWantReadError):
|
|
|
|
pass
|
|
|
|
class WebSocketWantWriteError(ssl.SSLWantWriteError):
|
|
|
|
pass
|
|
|
|
except:
|
|
|
|
class WebSocketWantReadError(OSError):
|
|
|
|
def __init__(self):
|
|
|
|
OSError.__init__(self, errno.EWOULDBLOCK)
|
|
|
|
class WebSocketWantWriteError(OSError):
|
|
|
|
def __init__(self):
|
|
|
|
OSError.__init__(self, errno.EWOULDBLOCK)
|
|
|
|
|
|
|
|
class WebSocket(object):
|
|
|
|
"""WebSocket protocol socket like class.
|
|
|
|
|
|
|
|
This provides access to the WebSocket protocol by behaving much
|
|
|
|
like a real socket would. It shares many similarities with
|
|
|
|
ssl.SSLSocket.
|
|
|
|
|
|
|
|
The WebSocket protocols requires extra data to be sent and received
|
|
|
|
compared to the application level data. This means that a socket
|
|
|
|
that is ready to be read may not hold enough data to decode any
|
|
|
|
application data, and a socket that is ready to be written to may
|
|
|
|
not have enough space for an entire WebSocket frame. This is
|
|
|
|
handled by the exceptions WebSocketWantReadError and
|
|
|
|
WebSocketWantWriteError. When these are raised the caller must wait
|
|
|
|
for the socket to become ready again and call the relevant function
|
|
|
|
again.
|
|
|
|
|
|
|
|
A connection is established by using either connect() or accept(),
|
|
|
|
depending on if a client or server session is desired. See the
|
|
|
|
respective functions for details.
|
|
|
|
|
|
|
|
The following methods are passed on to the underlying socket:
|
|
|
|
|
|
|
|
- fileno
|
|
|
|
- getpeername, getsockname
|
|
|
|
- getsockopt, setsockopt
|
|
|
|
- gettimeout, settimeout
|
|
|
|
- setblocking
|
2013-03-20 12:30:16 +00:00
|
|
|
"""
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2013-03-14 14:50:49 +00:00
|
|
|
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def __init__(self):
|
|
|
|
"""Creates an unconnected WebSocket"""
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._state = "new"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._partial_msg = ''.encode("ascii")
|
2012-04-25 19:44:01 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._recv_buffer = ''.encode("ascii")
|
|
|
|
self._recv_queue = []
|
|
|
|
self._send_buffer = ''.encode("ascii")
|
2011-09-29 22:08:28 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._sent_close = False
|
|
|
|
self._received_close = False
|
2011-09-29 22:08:28 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self.close_code = None
|
|
|
|
self.close_reason = None
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self.socket = None
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def __getattr__(self, name):
|
|
|
|
# These methods are just redirected to the underlying socket
|
|
|
|
if name in ["fileno",
|
|
|
|
"getpeername", "getsockname",
|
|
|
|
"getsockopt", "setsockopt",
|
|
|
|
"gettimeout", "settimeout",
|
|
|
|
"setblocking"]:
|
|
|
|
assert self.socket is not None
|
|
|
|
return getattr(self.socket, name)
|
|
|
|
else:
|
|
|
|
raise AttributeError("%s instance has no attribute '%s'" %
|
|
|
|
(self.__class__.__name__, name))
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def connect(self, uri, origin=None, protocols=[]):
|
|
|
|
"""Establishes a new connection to a WebSocket server.
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This method connects to the host specified by uri and
|
|
|
|
negotiates a WebSocket connection. origin should be specified
|
|
|
|
in accordance with RFC 6454 if known. A list of valid
|
|
|
|
sub-protocols can be specified in the protocols argument.
|
|
|
|
|
|
|
|
The data will be sent in the clear if the "ws" scheme is used,
|
|
|
|
and encrypted if the "wss" scheme is used.
|
|
|
|
|
|
|
|
Both WebSocketWantReadError and WebSocketWantWriteError can be
|
|
|
|
raised whilst negotiating the connection. Repeated calls to
|
|
|
|
connect() must retain the same arguments.
|
2011-05-02 04:17:04 +01:00
|
|
|
"""
|
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self.client = True;
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
uri = urlparse(uri)
|
2013-10-14 18:55:37 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
port = uri.port
|
|
|
|
if uri.scheme in ("ws", "http"):
|
|
|
|
if not port:
|
|
|
|
port = 80
|
|
|
|
elif uri.scheme in ("wss", "https"):
|
|
|
|
if not port:
|
|
|
|
port = 443
|
|
|
|
else:
|
|
|
|
raise Exception("Unknown scheme '%s'" % uri.scheme)
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
# This is a state machine in order to handle
|
|
|
|
# WantRead/WantWrite events
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "new":
|
|
|
|
self.socket = socket.create_connection((uri.hostname, port))
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if uri.scheme in ("wss", "https"):
|
|
|
|
self.socket = ssl.wrap_socket(self.socket)
|
|
|
|
self._state = "ssl_handshake"
|
|
|
|
else:
|
|
|
|
self._state = "headers"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "ssl_handshake":
|
|
|
|
self.socket.do_handshake()
|
|
|
|
self._state = "headers"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "headers":
|
|
|
|
self._key = ''
|
|
|
|
for i in range(16):
|
|
|
|
self._key += chr(random.randrange(256))
|
|
|
|
if sys.hexversion >= 0x3000000:
|
|
|
|
self._key = bytes(self._key, "latin-1")
|
|
|
|
self._key = b64encode(self._key).decode("ascii")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
path = uri.path
|
|
|
|
if not path:
|
|
|
|
path = "/"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._queue_str("GET %s HTTP/1.1\r\n" % path)
|
|
|
|
self._queue_str("Host: %s\r\n" % uri.hostname)
|
|
|
|
self._queue_str("Upgrade: websocket\r\n")
|
|
|
|
self._queue_str("Connection: upgrade\r\n")
|
|
|
|
self._queue_str("Sec-WebSocket-Key: %s\r\n" % self._key)
|
|
|
|
self._queue_str("Sec-WebSocket-Version: 13\r\n")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if origin is not None:
|
|
|
|
self._queue_str("Origin: %s\r\n" % origin)
|
|
|
|
if len(protocols) > 0:
|
|
|
|
self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % ", ".join(protocols))
|
2015-05-12 17:56:36 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._queue_str("\r\n")
|
2015-05-12 17:56:36 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._state = "send_headers"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "send_headers":
|
|
|
|
self._flush()
|
|
|
|
self._state = "response"
|
2011-05-02 22:50:17 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "response":
|
|
|
|
if not self._recv():
|
|
|
|
raise Exception("Socket closed unexpectedly")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._recv_buffer.find('\r\n\r\n'.encode("ascii")) == -1:
|
|
|
|
raise WebSocketWantReadError
|
2013-12-17 13:20:14 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
(request, self._recv_buffer) = self._recv_buffer.split('\r\n'.encode("ascii"), 1)
|
|
|
|
request = request.decode("latin-1")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
words = request.split()
|
|
|
|
if (len(words) < 2) or (words[0] != "HTTP/1.1"):
|
|
|
|
raise Exception("Invalid response")
|
|
|
|
if words[1] != "101":
|
|
|
|
raise Exception("WebSocket request denied: %s" % " ".join(words[1:]))
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
(headers, self._recv_buffer) = self._recv_buffer.split('\r\n\r\n'.encode("ascii"), 1)
|
|
|
|
headers = headers.decode('latin-1') + '\r\n'
|
|
|
|
headers = email.message_from_string(headers)
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if headers.get("Upgrade", "").lower() != "websocket":
|
|
|
|
print(type(headers))
|
|
|
|
raise Exception("Missing or incorrect upgrade header")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
accept = headers.get('Sec-WebSocket-Accept')
|
|
|
|
if accept is None:
|
|
|
|
raise Exception("Missing Sec-WebSocket-Accept header");
|
2011-06-26 19:55:52 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
expected = sha1((self._key + self.GUID).encode("ascii")).digest()
|
|
|
|
expected = b64encode(expected).decode("ascii")
|
2011-06-26 19:55:52 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
del self._key
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if accept != expected:
|
|
|
|
raise Exception("Invalid Sec-WebSocket-Accept header");
|
|
|
|
|
|
|
|
self._state = "done"
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
return
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
raise Exception("WebSocket is in an invalid state")
|
2011-05-02 04:17:04 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def accept(self, socket, headers):
|
|
|
|
"""Establishes a new WebSocket session with a client.
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This method negotiates a WebSocket connection with an incoming
|
|
|
|
client. The caller must provide the client socket and the
|
|
|
|
headers from the HTTP request.
|
|
|
|
|
|
|
|
A server can identify that a client is requesting a WebSocket
|
|
|
|
connection by looking at the "Upgrade" header. It will include
|
|
|
|
the value "websocket" in such cases.
|
|
|
|
|
|
|
|
WebSocketWantWriteError can be raised if the response cannot be
|
|
|
|
sent right away. Repeated calls to accept() does not need to
|
|
|
|
retain the arguments.
|
|
|
|
"""
|
2015-04-10 17:23:52 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
# This is a state machine in order to handle
|
|
|
|
# WantRead/WantWrite events
|
2015-04-10 19:14:31 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "new":
|
|
|
|
self.client = False
|
|
|
|
self.socket = socket
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if headers.get("upgrade", "").lower() != "websocket":
|
|
|
|
raise Exception("Missing or incorrect upgrade header")
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
ver = headers.get('Sec-WebSocket-Version')
|
|
|
|
if ver is None:
|
|
|
|
raise Exception("Missing Sec-WebSocket-Version header");
|
2012-07-13 19:09:32 +01:00
|
|
|
|
|
|
|
# HyBi-07 report version 7
|
|
|
|
# HyBi-08 - HyBi-12 report version 8
|
|
|
|
# HyBi-13 reports version 13
|
|
|
|
if ver in ['7', '8', '13']:
|
|
|
|
self.version = "hybi-%02d" % int(ver)
|
|
|
|
else:
|
2016-09-15 18:51:26 +01:00
|
|
|
raise Exception("Unsupported protocol version %s" % ver)
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
key = headers.get('Sec-WebSocket-Key')
|
|
|
|
if key is None:
|
|
|
|
raise Exception("Missing Sec-WebSocket-Key header");
|
2012-07-13 19:09:32 +01:00
|
|
|
|
|
|
|
# Generate the hash value for the accept header
|
2016-09-15 18:51:26 +01:00
|
|
|
accept = sha1((key + self.GUID).encode("ascii")).digest()
|
|
|
|
accept = b64encode(accept).decode("ascii")
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self.protocol = ''
|
|
|
|
protocols = headers.get('Sec-WebSocket-Protocol', '').split(',')
|
|
|
|
if protocols:
|
|
|
|
self.protocol = self.select_subprotocol(protocols)
|
2017-01-19 13:53:15 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._queue_str("HTTP/1.1 101 Switching Protocols\r\n")
|
|
|
|
self._queue_str("Upgrade: websocket\r\n")
|
|
|
|
self._queue_str("Connection: Upgrade\r\n")
|
|
|
|
self._queue_str("Sec-WebSocket-Accept: %s\r\n" % accept)
|
2017-01-19 13:53:15 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self.protocol:
|
|
|
|
self._queue_str("Sec-WebSocket-Protocol: %s\r\n" % self.protocol)
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._queue_str("\r\n")
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._state = "flush"
|
2015-08-25 21:44:24 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if self._state == "flush":
|
|
|
|
self._flush()
|
|
|
|
self._state = "done"
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
return
|
2015-08-25 21:44:24 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
raise Exception("WebSocket is in an invalid state")
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def select_subprotocol(self, protocols):
|
|
|
|
"""Returns which sub-protocol should be used.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This method does not select any sub-protocol by default and is
|
|
|
|
meant to be overridden by an implementation that wishes to make
|
|
|
|
use of sub-protocols. It will be called during handling of
|
|
|
|
accept().
|
|
|
|
"""
|
|
|
|
return ""
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def handle_ping(self, data):
|
|
|
|
"""Called when a WebSocket ping message is received.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This will be called whilst processing recv()/recvmsg(). The
|
|
|
|
default implementation sends a pong reply back."""
|
|
|
|
self.pong(data)
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def handle_pong(self, data):
|
|
|
|
"""Called when a WebSocket pong message is received.
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This will be called whilst processing recv()/recvmsg(). The
|
|
|
|
default implementation does nothing."""
|
|
|
|
pass
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def recv(self):
|
|
|
|
"""Read data from the WebSocket.
|
2012-07-13 19:09:32 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This will return any available data on the socket. If the
|
|
|
|
socket is closed then an empty buffer will be returned. The
|
|
|
|
reason for the close is found in the 'close_code' and
|
|
|
|
'close_reason' properties.
|
2014-04-14 13:20:00 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
Unlike recvmsg() this method may return data from more than one
|
|
|
|
WebSocket message. It is however not guaranteed to return all
|
|
|
|
buffered data. Callers should continue calling recv() whilst
|
|
|
|
pending() returns True.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
Both WebSocketWantReadError and WebSocketWantWriteError can be
|
|
|
|
raised when calling recv().
|
|
|
|
"""
|
|
|
|
return self.recvmsg()
|
2015-08-25 21:44:24 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def recvmsg(self):
|
|
|
|
"""Read a single message from the WebSocket.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This will return a single WebSocket message from the socket.
|
|
|
|
If the socket is closed then an empty buffer will be returned.
|
|
|
|
The reason for the close is found in the 'close_code' and
|
|
|
|
'close_reason' properties.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
Unlike recv() this method will not return data from more than
|
|
|
|
one WebSocket message. Callers should continue calling
|
|
|
|
recvmsg() whilst pending() returns True.
|
Try to solve https://github.com/kanaka/websockify/issues/71 by
refactoring. Basically, we are dividing WebSocketServer into two
classes: One request handler following the SocketServer Requesthandler
API, and one optional server engine. The standard Python SocketServer
engine can also be used.
websocketproxy.py has been updated to match the API change. I've also
added a new option --libserver in order to use the Python built in
server instead.
I've done a lot of testing with the new code. This includes: verbose,
daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've
tested both Python 2 and 3. I've also tested websocket.py in another
external service.
Code details follows:
* The new request handler class is called WebSocketRequestHandler,
inheriting SimpleHTTPRequestHandler.
* The service engine is called WebSocketServer, just like before.
* do_websocket_handshake: Using send_header() etc, instead of manually
sending HTTP response.
* A new method called handle_websocket() upgrades the connection to
WebSocket, if requested. Otherwise, it returns False. A typical
application use is:
def do_GET(self):
if not self.handle_websocket():
# handle normal requests
* new_client has been renamed to new_websocket_client, in order to
have a better name in the SocketServer/HTTPServer request handler
hierarchy.
* Note that in the request handler, configuration variables must be
provided by the "server" object, ie self.server.target_host.
2013-03-14 15:07:40 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
Both WebSocketWantReadError and WebSocketWantWriteError can be
|
|
|
|
raised when calling recvmsg().
|
|
|
|
"""
|
|
|
|
# May have been called to flush out a close
|
|
|
|
if self._received_close:
|
|
|
|
self._flush()
|
|
|
|
return ''.encode("ascii")
|
|
|
|
|
|
|
|
# Anything already queued?
|
|
|
|
if self.pending():
|
|
|
|
msg = self._recvmsg()
|
|
|
|
if msg is not None:
|
|
|
|
return msg
|
|
|
|
|
|
|
|
# Note: We cannot proceed to self._recv() here as we may
|
|
|
|
# have already called it once as part of the caller's
|
|
|
|
# "while websock.pending():" loop
|
|
|
|
raise WebSocketWantReadError
|
|
|
|
|
|
|
|
# Nope, let's try to read a bit
|
|
|
|
if not self._recv_frames():
|
|
|
|
return ''.encode("ascii")
|
|
|
|
|
|
|
|
# Anything queued now?
|
|
|
|
msg = self._recvmsg()
|
|
|
|
if msg is not None:
|
|
|
|
return msg
|
|
|
|
|
|
|
|
# Still nope
|
|
|
|
raise WebSocketWantReadError
|
|
|
|
|
|
|
|
def pending(self):
|
|
|
|
"""Check if any WebSocket data is pending.
|
|
|
|
|
|
|
|
This method will return True as long as there are WebSocket
|
|
|
|
frames that have yet been processed. A single recv() from the
|
|
|
|
underlying socket may return multiple WebSocket frames and it
|
|
|
|
is therefore important that a caller continues calling recv()
|
|
|
|
or recvmsg() as long as pending() returns True.
|
|
|
|
|
|
|
|
Note that this function merely tells if there are raw WebSocket
|
|
|
|
frames pending. Those frames may not contain any application
|
|
|
|
data.
|
|
|
|
"""
|
|
|
|
return len(self._recv_queue) > 0
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def send(self, bytes):
|
|
|
|
"""Write data to the WebSocket
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
This will queue the given data and attempt to send it to the
|
|
|
|
peer. Unlike sendmsg() this method might coalesce the data with
|
|
|
|
data from other calls, or split it over multiple messages.
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
WebSocketWantWriteError can be raised if there is insufficient
|
|
|
|
space in the underlying socket.
|
|
|
|
"""
|
|
|
|
return self.sendmsg(bytes)
|
2013-11-27 13:49:54 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def sendmsg(self, msg):
|
|
|
|
"""Write a single message to the WebSocket
|
|
|
|
|
|
|
|
This will queue the given message and attempt to send it to the
|
|
|
|
peer. Unlike send() this method will preserve the data as a
|
|
|
|
single WebSocket message.
|
|
|
|
|
|
|
|
WebSocketWantWriteError can be raised if there is insufficient
|
|
|
|
space in the underlying socket.
|
2013-03-14 14:50:49 +00:00
|
|
|
"""
|
2016-09-15 18:51:26 +01:00
|
|
|
if not self._sent_close:
|
|
|
|
# Only called to flush?
|
|
|
|
if msg:
|
|
|
|
self._sendmsg(0x2, msg)
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._flush()
|
|
|
|
return len(msg)
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def ping(self, data=None):
|
|
|
|
"""Write a ping message to the WebSocket."""
|
|
|
|
self._sendmg(0x9, data)
|
2015-12-02 16:59:00 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def pong(self, data=None):
|
|
|
|
"""Write a pong message to the WebSocket."""
|
|
|
|
self._sendmg(0xA, data)
|
|
|
|
|
|
|
|
def shutdown(self, how, code=1000, reason=None):
|
|
|
|
"""Gracefully terminate the WebSocket connection.
|
|
|
|
|
|
|
|
This will start the process to terminate the WebSocket
|
|
|
|
connection. The caller must continue to calling recv() or
|
|
|
|
recvmsg() after this function in order to wait for the peer to
|
|
|
|
acknowledge the close. Calls to send() and sendmsg() will be
|
|
|
|
ignored.
|
|
|
|
|
|
|
|
WebSocketWantWriteError can be raised if there is insufficient
|
|
|
|
space in the underlying socket for the close message.
|
|
|
|
|
|
|
|
The how argument is currently ignored.
|
2011-01-08 21:29:01 +00:00
|
|
|
"""
|
2016-09-15 18:51:26 +01:00
|
|
|
|
|
|
|
# Already closing?
|
|
|
|
if self._sent_close:
|
|
|
|
self._flush()
|
|
|
|
return
|
|
|
|
|
|
|
|
# Special code to indicate that we closed the connection
|
|
|
|
if not self._received_close:
|
|
|
|
self.close_code = 1000
|
|
|
|
self.close_reason = "Locally initiated close"
|
|
|
|
|
|
|
|
self._sent_close = True
|
|
|
|
|
|
|
|
msg = ''.encode('ascii')
|
|
|
|
if code is not None:
|
|
|
|
msg += struct.pack(">H", code)
|
|
|
|
if reason is not None:
|
|
|
|
msg += reason.encode("UTF-8")
|
|
|
|
|
|
|
|
self._sendmsg(0x8, msg)
|
|
|
|
|
|
|
|
def close(self, code=1000, reason=None):
|
|
|
|
"""Terminate the WebSocket connection immediately.
|
|
|
|
|
|
|
|
This will close the WebSocket connection directly after sending
|
|
|
|
a close message to the peer.
|
|
|
|
|
|
|
|
WebSocketWantWriteError can be raised if there is insufficient
|
|
|
|
space in the underlying socket for the close message.
|
2011-01-08 21:29:01 +00:00
|
|
|
"""
|
2016-09-15 18:51:26 +01:00
|
|
|
self.shutdown(socket.SHUT_RDWR, code, reason)
|
|
|
|
self._close()
|
|
|
|
|
|
|
|
def _recv(self):
|
|
|
|
# Fetches more data from the socket to the buffer
|
|
|
|
assert self.socket is not None
|
|
|
|
|
|
|
|
while True:
|
2011-01-08 21:29:01 +00:00
|
|
|
try:
|
2016-09-15 18:51:26 +01:00
|
|
|
data = self.socket.recv(4096)
|
|
|
|
except (socket.error, OSError):
|
|
|
|
exc = sys.exc_info()[1]
|
|
|
|
if hasattr(exc, 'errno'):
|
|
|
|
err = exc.errno
|
2011-01-08 21:29:01 +00:00
|
|
|
else:
|
2016-09-15 18:51:26 +01:00
|
|
|
err = exc[0]
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if err == errno.EWOULDBLOCK:
|
|
|
|
raise WebSocketWantReadError
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
raise
|
|
|
|
|
|
|
|
if len(data) == 0:
|
|
|
|
return False
|
2011-09-29 22:08:28 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
self._recv_buffer += data
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
# Support for SSLSocket like objects
|
|
|
|
if hasattr(self.socket, "pending"):
|
|
|
|
if not self.socket.pending():
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
break
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
return True
|
2011-01-08 21:29:01 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _recv_frames(self):
|
|
|
|
# Fetches more data and decodes the frames
|
|
|
|
if not self._recv():
|
|
|
|
if self.close_code is None:
|
|
|
|
self.close_code = 1006
|
|
|
|
self.close_reason = "Connection closed abnormally"
|
|
|
|
self._sent_close = self._received_close = True
|
|
|
|
self._close()
|
|
|
|
return False
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
while True:
|
|
|
|
frame = self._decode_hybi(self._recv_buffer)
|
|
|
|
if frame is None:
|
|
|
|
break
|
|
|
|
self._recv_buffer = self._recv_buffer[frame['length']:]
|
|
|
|
self._recv_queue.append(frame)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def _recvmsg(self):
|
|
|
|
# Process pending frames and returns any application data
|
|
|
|
while self._recv_queue:
|
|
|
|
frame = self._recv_queue.pop(0)
|
|
|
|
|
|
|
|
if not self.client and not frame['masked']:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame not masked")
|
|
|
|
continue
|
|
|
|
if self.client and frame['masked']:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Frame masked")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if frame["opcode"] == 0x0:
|
|
|
|
if not self._partial_msg:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected continuation frame")
|
|
|
|
continue
|
|
|
|
|
|
|
|
self._partial_msg += frame["payload"]
|
|
|
|
|
|
|
|
if frame["fin"]:
|
|
|
|
msg = self._partial_msg
|
|
|
|
self._partial_msg = ''.decode("ascii")
|
|
|
|
return msg
|
|
|
|
elif frame["opcode"] == 0x2:
|
|
|
|
if self._partial_msg:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Unexpected new frame")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if frame["fin"]:
|
|
|
|
return frame["payload"]
|
|
|
|
else:
|
|
|
|
self._partial_msg = frame["payload"]
|
|
|
|
elif frame["opcode"] == 0x8:
|
|
|
|
if self._received_close:
|
|
|
|
continue
|
|
|
|
|
|
|
|
self._received_close = True
|
|
|
|
|
|
|
|
if self._sent_close:
|
|
|
|
self._close()
|
|
|
|
return ''.encode("ascii")
|
|
|
|
|
|
|
|
if not frame["fin"]:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented close")
|
|
|
|
continue
|
|
|
|
|
|
|
|
code = None
|
|
|
|
reason = None
|
|
|
|
if len(frame["payload"]) >= 2:
|
|
|
|
code = struct.unpack(">H", frame["payload"][:2])
|
|
|
|
if len(frame["payload"]) > 2:
|
|
|
|
reason = frame["payload"][2:]
|
|
|
|
try:
|
|
|
|
reason = reason.decode("UTF-8")
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1002, "Procotol error: Invalid UTF-8 in close")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if code is None:
|
|
|
|
self.close_code = 1005
|
|
|
|
self.close_reason = "No close status code specified by peer"
|
|
|
|
else:
|
|
|
|
self.close_code = code
|
|
|
|
if reason is not None:
|
|
|
|
self.close_reason = reason
|
|
|
|
|
|
|
|
self.shutdown(code, reason)
|
|
|
|
return ''.encode("ascii")
|
|
|
|
elif frame["opcode"] == 0x9:
|
|
|
|
if not frame["fin"]:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented ping")
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.handle_ping(frame["payload"])
|
|
|
|
elif frame["opcode"] == 0xA:
|
|
|
|
if not frame["fin"]:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Fragmented pong")
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.handle_pong(frame["payload"])
|
|
|
|
else:
|
|
|
|
self.shutdown(socket.SHUT_RDWR, 1003, "Unsupported: Unknown opcode 0x%02x" % frame["opcode"])
|
2013-11-28 08:05:24 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
return None
|
2013-11-28 08:05:24 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _flush(self):
|
|
|
|
# Writes pending data to the socket
|
|
|
|
if not self._send_buffer:
|
|
|
|
return
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
assert self.socket is not None
|
2013-03-14 14:50:49 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
try:
|
|
|
|
sent = self.socket.send(self._send_buffer)
|
|
|
|
except (socket.error, OSError):
|
|
|
|
exc = sys.exc_info()[1]
|
|
|
|
if hasattr(exc, 'errno'):
|
|
|
|
err = exc.errno
|
|
|
|
else:
|
|
|
|
err = exc[0]
|
|
|
|
|
|
|
|
if err == errno.EWOULDBLOCK:
|
|
|
|
raise WebSocketWantWriteError
|
|
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
self._send_buffer = self._send_buffer[sent:]
|
|
|
|
|
|
|
|
if self._send_buffer:
|
|
|
|
raise WebSocketWantWriteError
|
|
|
|
|
|
|
|
# We had a pending close and we've flushed the buffer,
|
|
|
|
# time to end things
|
|
|
|
if self._received_close and self._sent_close:
|
|
|
|
self._close()
|
|
|
|
|
|
|
|
def _send(self, data):
|
|
|
|
# Queues data and attempts to send it
|
|
|
|
self._send_buffer += data
|
|
|
|
self._flush()
|
|
|
|
|
|
|
|
def _queue_str(self, string):
|
|
|
|
# Queue some data to be sent later.
|
|
|
|
# Only used by the connecting methods.
|
|
|
|
self._send_buffer += string.encode("latin-1")
|
|
|
|
|
|
|
|
def _sendmsg(self, opcode, msg):
|
|
|
|
# Sends a standard data message
|
|
|
|
if self.client:
|
|
|
|
mask = ''
|
|
|
|
for i in range(4):
|
|
|
|
mask += chr(random.randrange(256))
|
|
|
|
if sys.hexversion >= 0x3000000:
|
|
|
|
mask = bytes(mask, "latin-1")
|
|
|
|
frame = self._encode_hybi(opcode, msg, mask)
|
|
|
|
else:
|
|
|
|
frame = self._encode_hybi(opcode, msg)
|
wsproxy, wstelnet: wrap command, WS telnet client.
wswrapper:
Getting the wswrapper.c LD_PRELOAD model working has turned out to
involve too many dark corners of the glibc/POSIX file descriptor
space. I realized that 95% of what I want can be accomplished by
adding a "wrap command" mode to wsproxy.
The code is still there for now, but consider it experimental at
best. Minor fix to dup2 and add dup and dup3 logging.
wsproxy Wrap Command:
In wsproxy wrap command mode, a command line is specified instead
of a target address and port. wsproxy then uses a much simpler
LD_PRELOAD library, rebind.so, to move intercept any bind() system
calls made by the program. If the bind() call is for the wsproxy
listen port number then the real bind() system call is issued for
an alternate (free high) port on loopback/localhost. wsproxy then
forwards from the listen address/port to the moved port.
The --wrap-mode argument takes three options that determine the
behavior of wsproxy when the wrapped command returns an exit code
(exit or daemonizing): ignore, exit, respawn.
For example, this runs vncserver on turns port 5901 into
a WebSockets port (rebind.so must be built first):
./utils/wsproxy.py --wrap-mode=ignore 5901 -- vncserver :1
The vncserver command backgrounds itself so the wrap mode is set
to "ignore" so that wsproxy keeps running even after it receives
an exit code from vncserver.
wstelnet:
To demonstrate the wrap command mode, I added WebSockets telnet
client.
For example, this runs telnetd (krb5-telnetd) on turns port 2023
into a WebSockets port (using "respawn" mode since telnetd exits
after each connection closes):
sudo ./utils/wsproxy.py --wrap-mode=respawn 2023 -- telnetd -debug 2023
Then the utils/wstelnet.html page can be used to connect to the
telnetd server on port 2023. The telnet client includes VT100.js
(from http://code.google.com/p/sshconsole) which handles the
terminal emulation and rendering.
rebind:
The rebind LD_PRELOAD library is used by wsproxy in wrap command
mode to intercept bind() system calls and move the port to
a different port on loopback/localhost. The rebind.so library can
be built by running make in the utils directory.
The rebind library can be used separately from wsproxy by setting
the REBIND_OLD_PORT and REBIND_NEW_PORT environment variables
prior to executing a command. For example:
export export REBIND_PORT_OLD="23"
export export REBIND_PORT_NEW="65023"
LD_PRELOAD=./rebind.so telnetd -debug 23
Alternately, the rebind script does the same thing:
rebind 23 65023 telnetd -debug 23
Other changes/notes:
- wsproxy no longer daemonizes by default. Remove -f/--foreground
option and add -D/--deamon option.
- When wsproxy is used to wrap a command in "respawn" mode, the
command will not be respawn more often than 3 times within 10
seconds.
- Move getKeysym routine out of Canvas object so that it can be called
directly.
2011-01-12 19:15:11 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
return self._send(frame)
|
wsproxy, wstelnet: wrap command, WS telnet client.
wswrapper:
Getting the wswrapper.c LD_PRELOAD model working has turned out to
involve too many dark corners of the glibc/POSIX file descriptor
space. I realized that 95% of what I want can be accomplished by
adding a "wrap command" mode to wsproxy.
The code is still there for now, but consider it experimental at
best. Minor fix to dup2 and add dup and dup3 logging.
wsproxy Wrap Command:
In wsproxy wrap command mode, a command line is specified instead
of a target address and port. wsproxy then uses a much simpler
LD_PRELOAD library, rebind.so, to move intercept any bind() system
calls made by the program. If the bind() call is for the wsproxy
listen port number then the real bind() system call is issued for
an alternate (free high) port on loopback/localhost. wsproxy then
forwards from the listen address/port to the moved port.
The --wrap-mode argument takes three options that determine the
behavior of wsproxy when the wrapped command returns an exit code
(exit or daemonizing): ignore, exit, respawn.
For example, this runs vncserver on turns port 5901 into
a WebSockets port (rebind.so must be built first):
./utils/wsproxy.py --wrap-mode=ignore 5901 -- vncserver :1
The vncserver command backgrounds itself so the wrap mode is set
to "ignore" so that wsproxy keeps running even after it receives
an exit code from vncserver.
wstelnet:
To demonstrate the wrap command mode, I added WebSockets telnet
client.
For example, this runs telnetd (krb5-telnetd) on turns port 2023
into a WebSockets port (using "respawn" mode since telnetd exits
after each connection closes):
sudo ./utils/wsproxy.py --wrap-mode=respawn 2023 -- telnetd -debug 2023
Then the utils/wstelnet.html page can be used to connect to the
telnetd server on port 2023. The telnet client includes VT100.js
(from http://code.google.com/p/sshconsole) which handles the
terminal emulation and rendering.
rebind:
The rebind LD_PRELOAD library is used by wsproxy in wrap command
mode to intercept bind() system calls and move the port to
a different port on loopback/localhost. The rebind.so library can
be built by running make in the utils directory.
The rebind library can be used separately from wsproxy by setting
the REBIND_OLD_PORT and REBIND_NEW_PORT environment variables
prior to executing a command. For example:
export export REBIND_PORT_OLD="23"
export export REBIND_PORT_NEW="65023"
LD_PRELOAD=./rebind.so telnetd -debug 23
Alternately, the rebind script does the same thing:
rebind 23 65023 telnetd -debug 23
Other changes/notes:
- wsproxy no longer daemonizes by default. Remove -f/--foreground
option and add -D/--deamon option.
- When wsproxy is used to wrap a command in "respawn" mode, the
command will not be respawn more often than 3 times within 10
seconds.
- Move getKeysym routine out of Canvas object so that it can be called
directly.
2011-01-12 19:15:11 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _close(self):
|
|
|
|
# Close the underlying socket
|
|
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
2013-05-31 20:21:01 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _mask(self, buf, mask):
|
|
|
|
# Mask a frame
|
|
|
|
return self._unmask(buf, mask)
|
2013-10-21 21:50:52 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _unmask(self, buf, mask):
|
|
|
|
# Unmask a frame
|
|
|
|
if numpy:
|
|
|
|
plen = len(buf)
|
|
|
|
pstart = 0
|
|
|
|
pend = plen
|
|
|
|
b = c = ''.encode('ascii')
|
|
|
|
if plen >= 4:
|
|
|
|
dtype=numpy.dtype('<u4')
|
|
|
|
if sys.byteorder == 'big':
|
|
|
|
dtype = dtype.newbyteorder('>')
|
|
|
|
mask = numpy.frombuffer(mask, dtype, count=1)
|
|
|
|
data = numpy.frombuffer(buf, dtype, count=int(plen / 4))
|
|
|
|
#b = numpy.bitwise_xor(data, mask).data
|
|
|
|
b = numpy.bitwise_xor(data, mask).tostring()
|
2015-12-02 16:59:00 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
if plen % 4:
|
|
|
|
dtype=numpy.dtype('B')
|
|
|
|
if sys.byteorder == 'big':
|
|
|
|
dtype = dtype.newbyteorder('>')
|
|
|
|
mask = numpy.frombuffer(mask, dtype, count=(plen % 4))
|
|
|
|
data = numpy.frombuffer(buf, dtype,
|
|
|
|
offset=plen - (plen % 4), count=(plen % 4))
|
|
|
|
c = numpy.bitwise_xor(data, mask).tostring()
|
|
|
|
return b + c
|
|
|
|
else:
|
|
|
|
# Slower fallback
|
|
|
|
if sys.hexversion < 0x3000000:
|
|
|
|
mask = [ ord(c) for c in mask ]
|
|
|
|
data = array.array('B')
|
|
|
|
data.fromstring(buf)
|
|
|
|
for i in range(len(data)):
|
|
|
|
data[i] ^= mask[i % 4]
|
|
|
|
return data.tostring()
|
2015-12-02 16:59:00 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
def _encode_hybi(self, opcode, buf, mask_key=None, fin=True):
|
|
|
|
""" Encode a HyBi style WebSocket frame.
|
|
|
|
Optional opcode:
|
|
|
|
0x0 - continuation
|
|
|
|
0x1 - text frame
|
|
|
|
0x2 - binary frame
|
|
|
|
0x8 - connection close
|
|
|
|
0x9 - ping
|
|
|
|
0xA - pong
|
2011-01-08 21:29:01 +00:00
|
|
|
"""
|
2016-09-15 18:51:26 +01:00
|
|
|
|
|
|
|
b1 = opcode & 0x0f
|
|
|
|
if fin:
|
|
|
|
b1 |= 0x80
|
|
|
|
|
|
|
|
mask_bit = 0
|
|
|
|
if mask_key is not None:
|
|
|
|
mask_bit = 0x80
|
|
|
|
buf = self._mask(buf, mask_key)
|
|
|
|
|
|
|
|
payload_len = len(buf)
|
|
|
|
if payload_len <= 125:
|
|
|
|
header = struct.pack('>BB', b1, payload_len | mask_bit)
|
|
|
|
elif payload_len > 125 and payload_len < 65536:
|
|
|
|
header = struct.pack('>BBH', b1, 126 | mask_bit, payload_len)
|
|
|
|
elif payload_len >= 65536:
|
|
|
|
header = struct.pack('>BBQ', b1, 127 | mask_bit, payload_len)
|
|
|
|
|
|
|
|
if mask_key is not None:
|
|
|
|
return header + mask_key + buf
|
|
|
|
else:
|
|
|
|
return header + buf
|
|
|
|
|
|
|
|
def _decode_hybi(self, buf):
|
|
|
|
""" Decode HyBi style WebSocket packets.
|
|
|
|
Returns:
|
|
|
|
{'fin' : boolean,
|
|
|
|
'opcode' : number,
|
|
|
|
'masked' : boolean,
|
|
|
|
'length' : encoded_length,
|
|
|
|
'payload' : decoded_buffer}
|
2011-01-08 21:29:01 +00:00
|
|
|
"""
|
2013-10-14 19:13:11 +01:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
f = {'fin' : 0,
|
|
|
|
'opcode' : 0,
|
|
|
|
'masked' : False,
|
|
|
|
'length' : 0,
|
|
|
|
'payload' : None}
|
|
|
|
|
|
|
|
blen = len(buf)
|
|
|
|
hlen = 2
|
|
|
|
|
|
|
|
if blen < hlen:
|
|
|
|
return None
|
2011-01-07 00:26:54 +00:00
|
|
|
|
2016-09-15 18:51:26 +01:00
|
|
|
b1, b2 = struct.unpack(">BB", buf[:2])
|
|
|
|
f['opcode'] = b1 & 0x0f
|
|
|
|
f['fin'] = not not (b1 & 0x80)
|
|
|
|
f['masked'] = not not (b2 & 0x80)
|
|
|
|
|
|
|
|
if f['masked']:
|
|
|
|
hlen += 4
|
|
|
|
if blen < hlen:
|
|
|
|
return None
|
|
|
|
|
|
|
|
length = b2 & 0x7f
|
|
|
|
|
|
|
|
if length == 126:
|
|
|
|
hlen += 2
|
|
|
|
if blen < hlen:
|
|
|
|
return None
|
|
|
|
length, = struct.unpack('>H', buf[2:4])
|
|
|
|
elif length == 127:
|
|
|
|
hlen += 8
|
|
|
|
if blen < hlen:
|
|
|
|
return None
|
|
|
|
length, = struct.unpack('>Q', buf[2:10])
|
|
|
|
|
|
|
|
f['length'] = hlen + length
|
|
|
|
|
|
|
|
if blen < f['length']:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if f['masked']:
|
|
|
|
# unmask payload
|
|
|
|
mask_key = buf[hlen-4:hlen]
|
|
|
|
f['payload'] = self._unmask(buf[hlen:(hlen+length)], mask_key)
|
|
|
|
else:
|
|
|
|
f['payload'] = buf[hlen:(hlen+length)]
|
|
|
|
|
|
|
|
return f
|
2011-01-07 00:26:54 +00:00
|
|
|
|