%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/tests/ |
| Current File : //lib/calibre/calibre/srv/tests/web_sockets.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import socket, os, struct, errno, numbers
from collections import deque, namedtuple
from functools import partial
from hashlib import sha1
from calibre.srv.tests.base import BaseTest, TestServer
from calibre.srv.web_socket import (
GUID_STR, BINARY, TEXT, MessageWriter, create_frame, CLOSE, NORMAL_CLOSE,
PING, PONG, PROTOCOL_ERROR, CONTINUATION, INCONSISTENT_DATA, CONTROL_CODES)
from calibre.utils.monotonic import monotonic
from calibre.utils.socket_inheritance import set_socket_inherit
from polyglot.binary import as_base64_unicode
HANDSHAKE_STR = '''\
GET / HTTP/1.1\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Key: {}\r
Sec-WebSocket-Version: 13\r
''' + '\r\n'
Frame = namedtuple('Frame', 'fin opcode payload')
class WSClient:
def __init__(self, port, timeout=5):
self.timeout = timeout
self.socket = socket.create_connection(('localhost', port), timeout)
set_socket_inherit(self.socket, False)
self.key = as_base64_unicode(os.urandom(8))
self.socket.sendall(HANDSHAKE_STR.format(self.key).encode('ascii'))
self.read_buf = deque()
self.read_upgrade_response()
self.mask = memoryview(os.urandom(4))
self.frames = []
def read_upgrade_response(self):
from calibre.srv.http_request import read_headers
st = monotonic()
buf, idx = b'', -1
while idx == -1:
data = self.socket.recv(1024)
if not data:
raise ValueError('Server did not respond with a valid HTTP upgrade response')
buf += data
if len(buf) > 4096:
raise ValueError('Server responded with too much data to HTTP upgrade request')
if monotonic() - st > self.timeout:
raise ValueError('Timed out while waiting for server response to HTTP upgrade')
idx = buf.find(b'\r\n\r\n')
response, rest = buf[:idx+4], buf[idx+4:]
if rest:
self.read_buf.append(rest)
lines = (x + b'\r\n' for x in response.split(b'\r\n')[:-1])
rl = next(lines)
if rl != b'HTTP/1.1 101 Switching Protocols\r\n':
raise ValueError('Server did not respond with correct switching protocols line')
headers = read_headers(partial(next, lines))
key = as_base64_unicode(sha1((self.key + GUID_STR).encode('ascii')).digest())
if headers.get('Sec-WebSocket-Accept') != key:
raise ValueError('Server did not respond with correct key in Sec-WebSocket-Accept: {} != {}'.format(
key, headers.get('Sec-WebSocket-Accept')))
def recv(self, max_amt):
if self.read_buf:
data = self.read_buf.popleft()
if len(data) <= max_amt:
return data
self.read_buf.appendleft(data[max_amt+1:])
return data[:max_amt + 1]
try:
return self.socket.recv(max_amt)
except OSError as err:
if err.errno != errno.ECONNRESET:
raise
return b''
def read_size(self, size):
ans = b''
while len(ans) < size:
d = self.recv(size - len(ans))
if not d:
return None
ans += d
return ans
def read_frame(self):
x = self.read_size(2)
if x is None:
return None
b1, b2 = bytearray(x)
fin = bool(b1 & 0b10000000)
opcode = b1 & 0b1111
masked = b2 & 0b10000000
if masked:
raise ValueError('Got a frame with mask bit set from the server')
payload_length = b2 & 0b01111111
if payload_length == 126:
payload_length = struct.unpack(b'!H', self.read_size(2))[0]
elif payload_length == 127:
payload_length = struct.unpack(b'!Q', self.read_size(8))[0]
return Frame(fin, opcode, self.read_size(payload_length))
def read_messages(self):
messages, control_frames = [], []
msg_buf, opcode = [], None
while True:
frame = self.read_frame()
if frame is None or frame.payload is None:
break
if frame.opcode in CONTROL_CODES:
control_frames.append((frame.opcode, frame.payload))
else:
if opcode is None:
opcode = frame.opcode
msg_buf.append(frame.payload)
if frame.fin:
data = b''.join(msg_buf)
if opcode == TEXT:
data = data.decode('utf-8', 'replace')
messages.append((opcode, data))
msg_buf, opcode = [], None
return messages, control_frames
def write_message(self, msg, chunk_size=None):
if isinstance(msg, tuple):
opcode, msg = msg
if isinstance(msg, str):
msg = msg.encode('utf-8')
return self.write_frame(1, opcode, msg)
w = MessageWriter(msg, self.mask, chunk_size)
while True:
frame = w.create_frame()
if frame is None:
break
self.socket.sendall(frame.getvalue())
def write_frame(self, fin=1, opcode=CLOSE, payload=b'', rsv=0, mask=True):
frame = create_frame(fin, opcode, payload, rsv=(rsv << 4), mask=self.mask if mask else None)
self.socket.sendall(frame)
def write_close(self, code, reason=b''):
if isinstance(reason, str):
reason = reason.encode('utf-8')
self.write_frame(1, CLOSE, struct.pack(b'!H', code) + reason)
class WSTestServer(TestServer):
def __init__(self, handler):
TestServer.__init__(self, None, shutdown_timeout=5)
from calibre.srv.http_response import create_http_handler
self.loop.handler = create_http_handler(websocket_handler=handler())
@property
def ws_handler(self):
return self.loop.handler.websocket_handler
def connect(self):
return WSClient(self.address[1])
class WebSocketTest(BaseTest):
def simple_test(self, server, msgs, expected=(), close_code=NORMAL_CLOSE, send_close=True, close_reason=b'NORMAL CLOSE', ignore_send_failures=False):
client = server.connect()
for msg in msgs:
try:
if isinstance(msg, dict):
client.write_frame(**msg)
else:
client.write_message(msg)
except Exception:
if not ignore_send_failures:
raise
expected_messages, expected_controls = [], []
for ex in expected:
if isinstance(ex, str):
ex = TEXT, ex
elif isinstance(ex, bytes):
ex = BINARY, ex
elif isinstance(ex, numbers.Integral):
ex = ex, b''
if ex[0] in CONTROL_CODES:
expected_controls.append(ex)
else:
expected_messages.append(ex)
if send_close:
client.write_close(close_code, close_reason)
try:
messages, control_frames = client.read_messages()
except ConnectionAbortedError:
if expected_messages or expected_controls or send_close:
raise
return
self.ae(expected_messages, messages)
self.assertGreaterEqual(len(control_frames), 1)
self.ae(expected_controls, control_frames[:-1])
self.ae(control_frames[-1][0], CLOSE)
self.ae(close_code, struct.unpack_from(b'!H', control_frames[-1][1], 0)[0])
def test_websocket_basic(self):
'Test basic interaction with the websocket server'
from calibre.srv.web_socket import EchoHandler
with WSTestServer(EchoHandler) as server:
simple_test = partial(self.simple_test, server)
for q in ('', '*' * 125, '*' * 126, '*' * 127, '*' * 128, '*' * 65535, '*' * 65536, "Hello-µ@ßöäüàá-UTF-8!!"):
simple_test([q], [q])
for q in (b'', b'\xfe' * 125, b'\xfe' * 126, b'\xfe' * 127, b'\xfe' * 128, b'\xfe' * 65535, b'\xfe' * 65536):
simple_test([q], [q])
for payload in [b'', b'ping', b'\x00\xff\xfe\xfd\xfc\xfb\x00\xff', b"\xfe" * 125]:
simple_test([(PING, payload)], [(PONG, payload)])
simple_test([(PING, 'a'*126)], close_code=PROTOCOL_ERROR, send_close=False)
for payload in (b'', b'pong'):
simple_test([(PONG, payload)], [])
fragments = 'Hello-µ@ßöä üàá-UTF-8!!'.split()
nc = struct.pack(b'!H', NORMAL_CLOSE)
# It can happen that the server detects bad data and closes the
# connection before the client has finished sending all
# messages, so ignore failures to send packets.
isf_test = partial(simple_test, ignore_send_failures=True)
for rsv in range(1, 7):
isf_test([{'rsv':rsv, 'opcode':BINARY}], [], close_code=PROTOCOL_ERROR, send_close=False)
for opcode in (3, 4, 5, 6, 7, 11, 12, 13, 14, 15):
isf_test([{'opcode':opcode}], [], close_code=PROTOCOL_ERROR, send_close=False)
for opcode in (PING, PONG):
isf_test([
{'opcode':opcode, 'payload':'f1', 'fin':0}, {'opcode':opcode, 'payload':'f2'}
], close_code=PROTOCOL_ERROR, send_close=False)
isf_test([(CLOSE, nc + b'x'*124)], send_close=False, close_code=PROTOCOL_ERROR)
for fin in (0, 1):
isf_test([{'opcode':0, 'fin': fin, 'payload':b'non-continuation frame'}, 'some text'], close_code=PROTOCOL_ERROR, send_close=False)
isf_test([
{'opcode':TEXT, 'payload':fragments[0], 'fin':0}, {'opcode':CONTINUATION, 'payload':fragments[1]}, {'opcode':0, 'fin':0}
], [''.join(fragments)], close_code=PROTOCOL_ERROR, send_close=False)
isf_test([
{'opcode':TEXT, 'payload':fragments[0], 'fin':0}, {'opcode':TEXT, 'payload':fragments[1]},
], close_code=PROTOCOL_ERROR, send_close=False)
frags = []
for payload in (b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5', b'\xed\xa0\x80', b'\x80\x65\x64\x69\x74\x65\x64'):
frags.append({'opcode':(CONTINUATION if frags else TEXT), 'fin':1 if len(frags) == 2 else 0, 'payload':payload})
isf_test(frags, close_code=INCONSISTENT_DATA, send_close=False)
frags, q = [], b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5\xed\xa0\x80\x80\x65\x64\x69\x74\x65\x64'
for i in range(len(q)):
b = q[i:i+1]
frags.append({'opcode':(TEXT if i == 0 else CONTINUATION), 'fin':1 if i == len(q)-1 else 0, 'payload':b})
isf_test(frags, close_code=INCONSISTENT_DATA, send_close=False, ignore_send_failures=True)
for q in (b'\xce', b'\xce\xba\xe1'):
isf_test([{'opcode':TEXT, 'payload':q}], close_code=INCONSISTENT_DATA, send_close=False)
simple_test([
{'opcode':TEXT, 'payload':fragments[0], 'fin':0}, {'opcode':CONTINUATION, 'payload':fragments[1]}
], [''.join(fragments)])
simple_test([
{'opcode':TEXT, 'payload':fragments[0], 'fin':0}, (PING, b'pong'), {'opcode':CONTINUATION, 'payload':fragments[1]}
], [(PONG, b'pong'), ''.join(fragments)])
fragments = '12345'
simple_test([
{'opcode':TEXT, 'payload':fragments[0], 'fin':0}, {'opcode':CONTINUATION, 'payload':fragments[1], 'fin':0},
(PING, b'1'),
{'opcode':CONTINUATION, 'payload':fragments[2], 'fin':0}, {'opcode':CONTINUATION, 'payload':fragments[3], 'fin':0},
(PING, b'2'),
{'opcode':CONTINUATION, 'payload':fragments[4]}
], [(PONG, b'1'), (PONG, b'2'), fragments])
simple_test([
{'opcode':TEXT, 'fin':0}, {'opcode':CONTINUATION, 'fin':0}, {'opcode':CONTINUATION},], [''])
simple_test([
{'opcode':TEXT, 'fin':0}, {'opcode':CONTINUATION, 'fin':0, 'payload':'x'}, {'opcode':CONTINUATION},], ['x'])
for q in (b'\xc2\xb5', b'\xce\xba\xe1\xbd\xb9\xcf\x83\xce\xbc\xce\xb5', "Hello-µ@ßöäüàá-UTF-8!!".encode()):
frags = []
for i in range(len(q)):
b = q[i:i+1]
frags.append({'opcode':(TEXT if i == 0 else CONTINUATION), 'fin':1 if i == len(q)-1 else 0, 'payload':b})
simple_test(frags, [q.decode('utf-8')])
simple_test([(CLOSE, nc), (CLOSE, b'\x01\x01')], send_close=False)
simple_test([(CLOSE, nc), (PING, b'ping')], send_close=False)
simple_test([(CLOSE, nc), 'xxx'], send_close=False)
simple_test([{'opcode':TEXT, 'payload':'xxx', 'fin':0}, (CLOSE, nc), {'opcode':CONTINUATION, 'payload':'yyy'}], send_close=False)
simple_test([(CLOSE, b'')], send_close=False)
simple_test([(CLOSE, b'\x01')], send_close=False, close_code=PROTOCOL_ERROR)
simple_test([(CLOSE, nc + b'x'*123)], send_close=False)
simple_test([(CLOSE, nc + b'a\x80\x80')], send_close=False, close_code=PROTOCOL_ERROR)
for code in (1000,1001,1002,1003,1007,1008,1009,1010,1011,3000,3999,4000,4999):
simple_test([(CLOSE, struct.pack(b'!H', code))], send_close=False, close_code=code)
for code in (0,999,1004,1005,1006,1012,1013,1014,1015,1016,1100,2000,2999):
simple_test([(CLOSE, struct.pack(b'!H', code))], send_close=False, close_code=PROTOCOL_ERROR)
def test_websocket_perf(self):
from calibre.srv.web_socket import EchoHandler
with WSTestServer(EchoHandler) as server:
simple_test = partial(self.simple_test, server)
for sz in (64, 256, 1024, 4096, 8192, 16384):
sz *= 1024
t, b = 'a'*sz, b'a'*sz
simple_test([t, b], [t, b])
def find_tests():
import unittest
return unittest.defaultTestLoader.loadTestsFromTestCase(WebSocketTest)