291 lines
10 KiB
Python
291 lines
10 KiB
Python
from owrx.jsons import Encoder
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
from multiprocessing import Pipe
|
|
import select
|
|
import threading
|
|
from abc import ABC, abstractmethod
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
OPCODE_TEXT_MESSAGE = 0x01
|
|
OPCODE_BINARY_MESSAGE = 0x02
|
|
OPCODE_CLOSE = 0x08
|
|
OPCODE_PING = 0x09
|
|
OPCODE_PONG = 0x0A
|
|
|
|
|
|
class WebSocketException(IOError):
|
|
pass
|
|
|
|
|
|
class IncompleteRead(WebSocketException):
|
|
pass
|
|
|
|
|
|
class Drained(WebSocketException):
|
|
pass
|
|
|
|
|
|
class WebSocketClosed(WebSocketException):
|
|
pass
|
|
|
|
|
|
class Handler(ABC):
|
|
@abstractmethod
|
|
def handleTextMessage(self, connection, message: str):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def handleBinaryMessage(self, connection, data: bytes):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def handleClose(self):
|
|
pass
|
|
|
|
|
|
class WebSocketConnection(object):
|
|
connections = []
|
|
|
|
@staticmethod
|
|
def closeAll():
|
|
for c in WebSocketConnection.connections:
|
|
try:
|
|
c.close()
|
|
except:
|
|
logger.exception("exception while shutting down websocket connections")
|
|
|
|
def __init__(self, handler, messageHandler: Handler):
|
|
self.handler = handler
|
|
self.handler.connection.setblocking(0)
|
|
self.messageHandler = None
|
|
self.setMessageHandler(messageHandler)
|
|
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
|
|
self.open = True
|
|
self.sendLock = threading.Lock()
|
|
|
|
headers = {key.lower(): value for key, value in self.handler.headers.items()}
|
|
if "upgrade" not in headers:
|
|
raise WebSocketException("Upgrade header not found")
|
|
if headers["upgrade"].lower() != "websocket":
|
|
raise WebSocketException("Upgrade header does not contain expected value")
|
|
if "sec-websocket-key" not in headers:
|
|
raise WebSocketException("Websocket key not provided")
|
|
|
|
ws_key = headers["sec-websocket-key"]
|
|
shakey = hashlib.sha1()
|
|
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode())
|
|
ws_key_toreturn = base64.b64encode(shakey.digest())
|
|
self.handler.wfile.write(
|
|
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(
|
|
ws_key_toreturn.decode()
|
|
).encode()
|
|
)
|
|
self.pingTimer = None
|
|
self.resetPing()
|
|
|
|
def setMessageHandler(self, messageHandler: Handler):
|
|
self.messageHandler = messageHandler
|
|
|
|
def get_header(self, size, opcode):
|
|
ws_first_byte = 0b10000000 | (opcode & 0x0F)
|
|
if size > 2 ** 16 - 1:
|
|
# frame size can be increased up to 2^64 by setting the size to 127
|
|
# anything beyond that would need to be segmented into frames. i don't really think we'll need more.
|
|
return bytes(
|
|
[
|
|
ws_first_byte,
|
|
127,
|
|
(size >> 56) & 0xFF,
|
|
(size >> 48) & 0xFF,
|
|
(size >> 40) & 0xFF,
|
|
(size >> 32) & 0xFF,
|
|
(size >> 24) & 0xFF,
|
|
(size >> 16) & 0xFF,
|
|
(size >> 8) & 0xFF,
|
|
size & 0xFF,
|
|
]
|
|
)
|
|
elif size > 125:
|
|
# up to 2^16 can be sent using the extended payload size field by putting the size to 126
|
|
return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF])
|
|
else:
|
|
# 125 bytes binary message in a single unmasked frame
|
|
return bytes([ws_first_byte, size])
|
|
|
|
def send(self, data):
|
|
if not self.open:
|
|
raise WebSocketClosed()
|
|
# convenience
|
|
if type(data) == dict:
|
|
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
|
|
data = json.dumps(data, allow_nan=False, cls=Encoder)
|
|
|
|
# string-type messages are sent as text frames
|
|
if type(data) == str:
|
|
header = self.get_header(len(data), OPCODE_TEXT_MESSAGE)
|
|
data_to_send = header + data.encode("utf-8")
|
|
# anything else as binary
|
|
else:
|
|
header = self.get_header(len(data), OPCODE_BINARY_MESSAGE)
|
|
data_to_send = header + data
|
|
|
|
self._sendBytes(data_to_send)
|
|
|
|
def _sendBytes(self, data_to_send):
|
|
def chunks(input, n):
|
|
"""Yield successive n-sized chunks from input."""
|
|
for i in range(0, len(input), n):
|
|
yield input[i: i + n]
|
|
|
|
try:
|
|
with self.sendLock:
|
|
for chunk in chunks(data_to_send, 1024):
|
|
(_, write, _) = select.select([], [self.handler.wfile], [], 10)
|
|
if self.handler.wfile in write:
|
|
written = self.handler.wfile.write(chunk)
|
|
if written != len(chunk):
|
|
logger.error("incomplete write! closing socket!")
|
|
self.close()
|
|
break
|
|
else:
|
|
logger.debug("socket not returned from select; closing")
|
|
self.close()
|
|
break
|
|
# these exception happen when the socket is closed
|
|
except OSError:
|
|
logger.exception("OSError while writing data")
|
|
self.close()
|
|
except ValueError:
|
|
logger.exception("ValueError while writing data")
|
|
self.close()
|
|
|
|
def interrupt(self):
|
|
if self.interruptPipeSend is None:
|
|
logger.debug("interrupt with closed pipe")
|
|
return
|
|
self.interruptPipeSend.send(bytes(0x00))
|
|
|
|
def handle(self):
|
|
WebSocketConnection.connections.append(self)
|
|
try:
|
|
self.read_loop()
|
|
finally:
|
|
logger.debug("websocket loop ended; shutting down")
|
|
|
|
self.messageHandler.handleClose()
|
|
self.cancelPing()
|
|
|
|
logger.debug("websocket loop ended; sending close frame")
|
|
|
|
header = self.get_header(0, OPCODE_CLOSE)
|
|
self._sendBytes(header)
|
|
|
|
try:
|
|
WebSocketConnection.connections.remove(self)
|
|
except ValueError:
|
|
pass
|
|
|
|
def read_loop(self):
|
|
def protected_read(num):
|
|
data = self.handler.rfile.read(num)
|
|
if data is None:
|
|
raise Drained()
|
|
if len(data) != num:
|
|
raise IncompleteRead()
|
|
return data
|
|
|
|
self.open = True
|
|
while self.open:
|
|
(read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [], 15)
|
|
if self.handler.rfile in read:
|
|
available = True
|
|
self.resetPing()
|
|
while self.open and available:
|
|
try:
|
|
header = protected_read(2)
|
|
opcode = header[0] & 0x0F
|
|
length = header[1] & 0x7F
|
|
mask = (header[1] & 0x80) >> 7
|
|
if length == 126:
|
|
header = protected_read(2)
|
|
length = (header[0] << 8) + header[1]
|
|
if mask:
|
|
masking_key = protected_read(4)
|
|
data = protected_read(length)
|
|
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
|
|
else:
|
|
data = protected_read(length)
|
|
if opcode == OPCODE_TEXT_MESSAGE:
|
|
message = data.decode("utf-8")
|
|
try:
|
|
self.messageHandler.handleTextMessage(self, message)
|
|
except Exception:
|
|
logger.exception("Exception in websocket handler handleTextMessage()")
|
|
elif opcode == OPCODE_BINARY_MESSAGE:
|
|
try:
|
|
self.messageHandler.handleBinaryMessage(self, data)
|
|
except Exception:
|
|
logger.exception("Exception in websocket handler handleBinaryMessage()")
|
|
elif opcode == OPCODE_PING:
|
|
self.sendPong()
|
|
elif opcode == OPCODE_PONG:
|
|
# since every read resets the ping timer, there's nothing to do here.
|
|
pass
|
|
elif opcode == OPCODE_CLOSE:
|
|
logger.debug("websocket close frame received; closing connection")
|
|
self.open = False
|
|
else:
|
|
logger.warning("unsupported opcode: {0}".format(opcode))
|
|
except Drained:
|
|
available = False
|
|
except IncompleteRead:
|
|
logger.warning("incomplete read on websocket; closing connection")
|
|
self.open = False
|
|
except OSError:
|
|
logger.exception("OSError while reading data; closing connection")
|
|
self.open = False
|
|
|
|
self.interruptPipeSend.close()
|
|
self.interruptPipeSend = None
|
|
# drain messages left in the queue so that the queue can be successfully closed
|
|
# this is necessary since python keeps the file descriptors open otherwise
|
|
try:
|
|
while True:
|
|
self.interruptPipeRecv.recv()
|
|
except EOFError:
|
|
pass
|
|
self.interruptPipeRecv.close()
|
|
self.interruptPipeRecv = None
|
|
|
|
def close(self):
|
|
if not self.open:
|
|
return
|
|
self.open = False
|
|
self.interrupt()
|
|
|
|
def cancelPing(self):
|
|
if self.pingTimer:
|
|
self.pingTimer.cancel()
|
|
|
|
def resetPing(self):
|
|
self.cancelPing()
|
|
if not self.open:
|
|
logger.debug("resetPing() while closed. passing...")
|
|
return
|
|
self.pingTimer = threading.Timer(30, self.sendPing)
|
|
self.pingTimer.start()
|
|
|
|
def sendPing(self):
|
|
header = self.get_header(0, OPCODE_PING)
|
|
self._sendBytes(header)
|
|
self.resetPing()
|
|
|
|
def sendPong(self):
|
|
header = self.get_header(0, OPCODE_PONG)
|
|
self._sendBytes(header)
|