2017-11-27 11:00:34 +03:00
|
|
|
# coding: utf8
|
|
|
|
|
|
|
|
import collections
|
2018-02-04 23:08:57 +03:00
|
|
|
import time
|
2017-11-27 11:00:34 +03:00
|
|
|
from urllib import parse
|
|
|
|
|
|
|
|
from hypothesis import given, settings
|
|
|
|
|
|
|
|
import http_strategies
|
|
|
|
from http_base import DaphneTestCase, DaphneTestingInstance
|
|
|
|
|
|
|
|
|
|
|
|
class TestWebsocket(DaphneTestCase):
|
|
|
|
"""
|
2017-11-27 11:02:37 +03:00
|
|
|
Tests WebSocket handshake, send and receive.
|
2017-11-27 11:00:34 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
def assert_valid_websocket_scope(
|
2018-08-27 05:27:32 +03:00
|
|
|
self, scope, path="/", params=None, headers=None, scheme=None, subprotocols=None
|
2017-11-29 10:42:35 +03:00
|
|
|
):
|
2017-11-27 11:00:34 +03:00
|
|
|
"""
|
|
|
|
Checks that the passed scope is a valid ASGI HTTP scope regarding types
|
|
|
|
and some urlencoding things.
|
|
|
|
"""
|
|
|
|
# Check overall keys
|
|
|
|
self.assert_key_sets(
|
2019-07-03 21:22:03 +03:00
|
|
|
required_keys={"type", "path", "raw_path", "query_string", "headers"},
|
2017-11-27 11:00:34 +03:00
|
|
|
optional_keys={"scheme", "root_path", "client", "server", "subprotocols"},
|
|
|
|
actual_keys=scope.keys(),
|
|
|
|
)
|
|
|
|
# Check that it is the right type
|
|
|
|
self.assertEqual(scope["type"], "websocket")
|
|
|
|
# Path
|
2019-07-03 21:22:03 +03:00
|
|
|
self.assert_valid_path(scope["path"])
|
2017-11-27 11:00:34 +03:00
|
|
|
# Scheme
|
|
|
|
self.assertIn(scope.get("scheme", "ws"), ["ws", "wss"])
|
|
|
|
if scheme:
|
|
|
|
self.assertEqual(scheme, scope["scheme"])
|
|
|
|
# Query string (byte string and still url encoded)
|
|
|
|
query_string = scope["query_string"]
|
|
|
|
self.assertIsInstance(query_string, bytes)
|
|
|
|
if params:
|
2018-08-27 05:27:32 +03:00
|
|
|
self.assertEqual(
|
|
|
|
query_string, parse.urlencode(params or []).encode("ascii")
|
|
|
|
)
|
2017-11-27 11:00:34 +03:00
|
|
|
# Ordering of header names is not important, but the order of values for a header
|
|
|
|
# name is. To assert whether that order is kept, we transform both the request
|
|
|
|
# headers and the channel message headers into a dictionary
|
|
|
|
# {name: [value1, value2, ...]} and check if they're equal.
|
|
|
|
transformed_scope_headers = collections.defaultdict(list)
|
|
|
|
for name, value in scope["headers"]:
|
2018-02-02 08:18:13 +03:00
|
|
|
transformed_scope_headers.setdefault(name, [])
|
2018-02-02 08:02:27 +03:00
|
|
|
# Make sure to split out any headers collapsed with commas
|
|
|
|
for bit in value.split(b","):
|
|
|
|
if bit.strip():
|
|
|
|
transformed_scope_headers[name].append(bit.strip())
|
2017-11-27 11:00:34 +03:00
|
|
|
transformed_request_headers = collections.defaultdict(list)
|
2018-08-27 05:27:32 +03:00
|
|
|
for name, value in headers or []:
|
2018-09-28 19:45:03 +03:00
|
|
|
expected_name = name.lower().strip()
|
|
|
|
expected_value = value.strip()
|
2018-02-02 08:02:27 +03:00
|
|
|
# Make sure to split out any headers collapsed with commas
|
2018-02-02 08:18:13 +03:00
|
|
|
transformed_request_headers.setdefault(expected_name, [])
|
2018-02-02 08:02:27 +03:00
|
|
|
for bit in expected_value.split(b","):
|
|
|
|
if bit.strip():
|
|
|
|
transformed_request_headers[expected_name].append(bit.strip())
|
2017-11-27 11:00:34 +03:00
|
|
|
for name, value in transformed_request_headers.items():
|
|
|
|
self.assertIn(name, transformed_scope_headers)
|
|
|
|
self.assertEqual(value, transformed_scope_headers[name])
|
|
|
|
# Root path
|
|
|
|
self.assertIsInstance(scope.get("root_path", ""), str)
|
|
|
|
# Client and server addresses
|
|
|
|
client = scope.get("client")
|
|
|
|
if client is not None:
|
|
|
|
self.assert_valid_address_and_port(client)
|
|
|
|
server = scope.get("server")
|
|
|
|
if server is not None:
|
|
|
|
self.assert_valid_address_and_port(server)
|
|
|
|
# Subprotocols
|
|
|
|
scope_subprotocols = scope.get("subprotocols", [])
|
|
|
|
if scope_subprotocols:
|
|
|
|
assert all(isinstance(x, str) for x in scope_subprotocols)
|
|
|
|
if subprotocols:
|
|
|
|
assert sorted(scope_subprotocols) == sorted(subprotocols)
|
|
|
|
|
|
|
|
def assert_valid_websocket_connect_message(self, message):
|
|
|
|
"""
|
|
|
|
Asserts that a message is a valid http.request message
|
|
|
|
"""
|
|
|
|
# Check overall keys
|
|
|
|
self.assert_key_sets(
|
2018-08-27 05:27:32 +03:00
|
|
|
required_keys={"type"}, optional_keys=set(), actual_keys=message.keys()
|
2017-11-27 11:00:34 +03:00
|
|
|
)
|
|
|
|
# Check that it is the right type
|
|
|
|
self.assertEqual(message["type"], "websocket.connect")
|
|
|
|
|
|
|
|
def test_accept(self):
|
|
|
|
"""
|
|
|
|
Tests we can open and accept a socket.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2017-11-27 11:00:34 +03:00
|
|
|
self.websocket_handshake(test_app)
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_scope(scope)
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
|
|
|
|
def test_reject(self):
|
|
|
|
"""
|
|
|
|
Tests we can reject a socket and it won't complete the handshake.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.close"}])
|
2017-11-27 11:00:34 +03:00
|
|
|
with self.assertRaises(RuntimeError):
|
|
|
|
self.websocket_handshake(test_app)
|
|
|
|
|
|
|
|
def test_subprotocols(self):
|
|
|
|
"""
|
|
|
|
Tests that we can ask for subprotocols and then select one.
|
|
|
|
"""
|
|
|
|
subprotocols = ["proto1", "proto2"]
|
|
|
|
with DaphneTestingInstance() as test_app:
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages(
|
|
|
|
[{"type": "websocket.accept", "subprotocol": "proto2"}]
|
|
|
|
)
|
|
|
|
_, subprotocol = self.websocket_handshake(
|
|
|
|
test_app, subprotocols=subprotocols
|
|
|
|
)
|
2017-11-27 11:00:34 +03:00
|
|
|
# Validate the scope and messages we got
|
|
|
|
assert subprotocol == "proto2"
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_scope(scope, subprotocols=subprotocols)
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
|
2020-09-14 13:33:13 +03:00
|
|
|
def test_accept_permessage_deflate_extension(self):
|
|
|
|
"""
|
|
|
|
Tests that permessage-deflate extension is successfuly accepted
|
|
|
|
by underlying `autobahn` package.
|
|
|
|
"""
|
|
|
|
|
|
|
|
headers = [
|
|
|
|
(
|
|
|
|
b"Sec-WebSocket-Extensions",
|
|
|
|
b"permessage-deflate; client_max_window_bits",
|
|
|
|
),
|
|
|
|
]
|
|
|
|
|
|
|
|
with DaphneTestingInstance() as test_app:
|
|
|
|
test_app.add_send_messages(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"type": "websocket.accept",
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
sock, subprotocol = self.websocket_handshake(
|
|
|
|
test_app,
|
|
|
|
headers=headers,
|
|
|
|
)
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
|
|
|
|
def test_accept_custom_extension(self):
|
|
|
|
"""
|
|
|
|
Tests that custom headers can be accpeted during handshake.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
|
|
|
test_app.add_send_messages(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"type": "websocket.accept",
|
|
|
|
"headers": [(b"Sec-WebSocket-Extensions", b"custom-extension")],
|
|
|
|
}
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
sock, subprotocol = self.websocket_handshake(
|
|
|
|
test_app,
|
|
|
|
headers=[
|
|
|
|
(b"Sec-WebSocket-Extensions", b"custom-extension"),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
|
2018-05-26 13:16:07 +03:00
|
|
|
def test_xff(self):
|
|
|
|
"""
|
|
|
|
Tests that X-Forwarded-For headers get parsed right
|
|
|
|
"""
|
2018-08-27 05:27:32 +03:00
|
|
|
headers = [["X-Forwarded-For", "10.1.2.3"], ["X-Forwarded-Port", "80"]]
|
2018-05-26 13:16:07 +03:00
|
|
|
with DaphneTestingInstance(xff=True) as test_app:
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2018-05-26 13:16:07 +03:00
|
|
|
self.websocket_handshake(test_app, headers=headers)
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_scope(scope)
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
assert scope["client"] == ["10.1.2.3", 80]
|
|
|
|
|
2017-11-27 11:00:34 +03:00
|
|
|
@given(
|
|
|
|
request_path=http_strategies.http_path(),
|
|
|
|
request_params=http_strategies.query_params(),
|
|
|
|
request_headers=http_strategies.headers(),
|
|
|
|
)
|
|
|
|
@settings(max_examples=5, deadline=2000)
|
2018-08-27 05:27:32 +03:00
|
|
|
def test_http_bits(self, request_path, request_params, request_headers):
|
2017-11-27 11:00:34 +03:00
|
|
|
"""
|
|
|
|
Tests that various HTTP-level bits (query string params, path, headers)
|
|
|
|
carry over into the scope.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2017-11-27 11:00:34 +03:00
|
|
|
self.websocket_handshake(
|
|
|
|
test_app,
|
2019-07-03 21:22:03 +03:00
|
|
|
path=parse.quote(request_path),
|
2017-11-27 11:00:34 +03:00
|
|
|
params=request_params,
|
|
|
|
headers=request_headers,
|
|
|
|
)
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_scope(
|
2018-08-27 05:27:32 +03:00
|
|
|
scope, path=request_path, params=request_params, headers=request_headers
|
2017-11-27 11:00:34 +03:00
|
|
|
)
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
|
2019-07-03 21:22:03 +03:00
|
|
|
def test_raw_path(self):
|
|
|
|
"""
|
|
|
|
Tests that /foo%2Fbar produces raw_path and a decoded path
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
|
|
|
self.websocket_handshake(test_app, path="/foo%2Fbar")
|
|
|
|
# Validate the scope and messages we got
|
|
|
|
scope, _ = test_app.get_received()
|
|
|
|
|
|
|
|
self.assertEqual(scope["path"], "/foo/bar")
|
|
|
|
self.assertEqual(scope["raw_path"], b"/foo%2Fbar")
|
|
|
|
|
2017-11-27 11:00:34 +03:00
|
|
|
def test_text_frames(self):
|
|
|
|
"""
|
|
|
|
Tests we can send and receive text frames.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
|
|
|
# Connect
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2017-11-27 11:00:34 +03:00
|
|
|
sock, _ = self.websocket_handshake(test_app)
|
|
|
|
_, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
# Prep frame for it to send
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages(
|
|
|
|
[{"type": "websocket.send", "text": "here be dragons 🐉"}]
|
|
|
|
)
|
2017-11-27 11:00:34 +03:00
|
|
|
# Send it a frame
|
|
|
|
self.websocket_send_frame(sock, "what is here? 🌍")
|
|
|
|
# Receive a frame and make sure it's correct
|
|
|
|
assert self.websocket_receive_frame(sock) == "here be dragons 🐉"
|
|
|
|
# Make sure it got our frame
|
|
|
|
_, messages = test_app.get_received()
|
2018-08-27 05:27:32 +03:00
|
|
|
assert messages[1] == {
|
|
|
|
"type": "websocket.receive",
|
|
|
|
"text": "what is here? 🌍",
|
|
|
|
}
|
2017-11-27 11:00:34 +03:00
|
|
|
|
|
|
|
def test_binary_frames(self):
|
|
|
|
"""
|
|
|
|
Tests we can send and receive binary frames with things that are very
|
|
|
|
much not valid UTF-8.
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance() as test_app:
|
|
|
|
# Connect
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2017-11-27 11:00:34 +03:00
|
|
|
sock, _ = self.websocket_handshake(test_app)
|
|
|
|
_, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
# Prep frame for it to send
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages(
|
|
|
|
[{"type": "websocket.send", "bytes": b"here be \xe2 bytes"}]
|
|
|
|
)
|
2017-11-27 11:00:34 +03:00
|
|
|
# Send it a frame
|
|
|
|
self.websocket_send_frame(sock, b"what is here? \xe2")
|
|
|
|
# Receive a frame and make sure it's correct
|
|
|
|
assert self.websocket_receive_frame(sock) == b"here be \xe2 bytes"
|
|
|
|
# Make sure it got our frame
|
|
|
|
_, messages = test_app.get_received()
|
2018-08-27 05:27:32 +03:00
|
|
|
assert messages[1] == {
|
|
|
|
"type": "websocket.receive",
|
|
|
|
"bytes": b"what is here? \xe2",
|
|
|
|
}
|
2018-02-04 23:08:57 +03:00
|
|
|
|
|
|
|
def test_http_timeout(self):
|
|
|
|
"""
|
|
|
|
Tests that the HTTP timeout doesn't kick in for WebSockets
|
|
|
|
"""
|
|
|
|
with DaphneTestingInstance(http_timeout=1) as test_app:
|
|
|
|
# Connect
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.accept"}])
|
2018-02-04 23:08:57 +03:00
|
|
|
sock, _ = self.websocket_handshake(test_app)
|
|
|
|
_, messages = test_app.get_received()
|
|
|
|
self.assert_valid_websocket_connect_message(messages[0])
|
|
|
|
# Wait 2 seconds
|
|
|
|
time.sleep(2)
|
|
|
|
# Prep frame for it to send
|
2018-08-27 05:27:32 +03:00
|
|
|
test_app.add_send_messages([{"type": "websocket.send", "text": "cake"}])
|
2018-02-04 23:08:57 +03:00
|
|
|
# Send it a frame
|
|
|
|
self.websocket_send_frame(sock, "still alive?")
|
|
|
|
# Receive a frame and make sure it's correct
|
|
|
|
assert self.websocket_receive_frame(sock) == "cake"
|