import base64
import hashlib
import itertools
import random
import select
import struct
import pytest
from unit.applications.proto import TestApplicationProto
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
class TestApplicationWebsocket(TestApplicationProto):
OP_CONT = 0x00
OP_TEXT = 0x01
OP_BINARY = 0x02
OP_CLOSE = 0x08
OP_PING = 0x09
OP_PONG = 0x0A
CLOSE_CODES = [1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011]
def key(self):
raw_key = bytes(random.getrandbits(8) for _ in range(16))
return base64.b64encode(raw_key).decode()
def accept(self, key):
sha1 = hashlib.sha1((key + GUID).encode()).digest()
return base64.b64encode(sha1).decode()
def upgrade(self, headers=None):
key = None
if headers is None:
key = self.key()
headers = {
'Host': 'localhost',
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Key': key,
'Sec-WebSocket-Protocol': 'chat, phone, video',
'Sec-WebSocket-Version': 13,
}
sock = self.get(
headers=headers,
no_recv=True,
)
resp = ''
while True:
rlist = select.select([sock], [], [], 60)[0]
if not rlist:
pytest.fail("Can't read response from server.")
resp += sock.recv(4096).decode()
if resp.startswith('HTTP/') and '\r\n\r\n' in resp:
resp = self._resp_to_dict(resp)
break
return (resp, sock, key)
def apply_mask(self, data, mask):
return bytes(b ^ m for b, m in zip(data, itertools.cycle(mask)))
def serialize_close(self, code=1000, reason=''):
return struct.pack('!H', code) + reason.encode('utf-8')
def frame_read(self, sock, read_timeout=60):
def recv_bytes(sock, bytes):
data = b''
while True:
rlist = select.select([sock], [], [], read_timeout)[0]
if not rlist:
# For all current cases if the "read_timeout" was changed
# than test do not expect to get a response from server.
if read_timeout == 60:
pytest.fail("Can't read response from server.")
break
data += sock.recv(bytes - len(data))
if len(data) == bytes:
break
return data
frame = {}
(head1,) = struct.unpack('!B', recv_bytes(sock, 1))
(head2,) = struct.unpack('!B', recv_bytes(sock, 1))
frame['fin'] = bool(head1 & 0b10000000)
frame['rsv1'] = bool(head1 & 0b01000000)
frame['rsv2'] = bool(head1 & 0b00100000)
frame['rsv3'] = bool(head1 & 0b00010000)
frame['opcode'] = head1 & 0b00001111
frame['mask'] = head2 & 0b10000000
length = head2 & 0b01111111
if length == 126:
data = recv_bytes(sock, 2)
(length,) = struct.unpack('!H', data)
elif length == 127:
data = recv_bytes(sock, 8)
(length,) = struct.unpack('!Q', data)
if frame['mask']:
mask_bits = recv_bytes(sock, 4)
data = b''
if length != 0:
data = recv_bytes(sock, length)
if frame['mask']:
data = self.apply_mask(data, mask_bits)
if frame['opcode'] == self.OP_CLOSE:
if length >= 2:
(code,) = struct.unpack('!H', data[:2])
reason = data[2:].decode('utf-8')
if not (code in self.CLOSE_CODES or 3000 <= code < 5000):
pytest.fail('Invalid status code')
frame['code'] = code
frame['reason'] = reason
elif length == 0:
frame['code'] = 1005
frame['reason'] = ''
else:
pytest.fail('Close frame too short')
frame['data'] = data
if frame['mask']:
pytest.fail('Received frame with mask')
return frame
def frame_to_send(
self,
opcode,
data,
fin=True,
length=None,
rsv1=False,
rsv2=False,
rsv3=False,
mask=True,
):
frame = b''
if isinstance(data, str):
data = data.encode('utf-8')
head1 = (
(0b10000000 if fin else 0)
| (0b01000000 if rsv1 else 0)
| (0b00100000 if rsv2 else 0)
| (0b00010000 if rsv3 else 0)
| opcode
)
head2 = 0b10000000 if mask else 0
data_length = len(data) if length is None else length
if data_length < 126:
frame += struct.pack('!BB', head1, head2 | data_length)
elif data_length < 65536:
frame += struct.pack('!BBH', head1, head2 | 126, data_length)
else:
frame += struct.pack('!BBQ', head1, head2 | 127, data_length)
if mask:
mask_bits = struct.pack('!I', random.getrandbits(32))
frame += mask_bits
if mask:
frame += self.apply_mask(data, mask_bits)
else:
frame += data
return frame
def frame_write(self, sock, *args, **kwargs):
chopsize = kwargs.pop('chopsize') if 'chopsize' in kwargs else None
frame = self.frame_to_send(*args, **kwargs)
if chopsize is None:
try:
sock.sendall(frame)
except BrokenPipeError:
pass
else:
pos = 0
frame_len = len(frame)
while pos < frame_len:
end = min(pos + chopsize, frame_len)
try:
sock.sendall(frame[pos:end])
except BrokenPipeError:
end = frame_len
pos = end
def message(self, sock, type, message, fragmention_size=None, **kwargs):
message_len = len(message)
if fragmention_size is None:
fragmention_size = message_len
if message_len <= fragmention_size:
self.frame_write(sock, type, message, **kwargs)
return
pos = 0
op_code = type
while pos < message_len:
end = min(pos + fragmention_size, message_len)
fin = end == message_len
self.frame_write(sock, op_code, message[pos:end], fin=fin, **kwargs)
op_code = self.OP_CONT
pos = end
def message_read(self, sock, read_timeout=60):
frame = self.frame_read(sock, read_timeout=read_timeout)
while not frame['fin']:
temp = self.frame_read(sock, read_timeout=read_timeout)
frame['data'] += temp['data']
frame['fin'] = temp['fin']
return frame