mirror of
https://github.com/django/daphne.git
synced 2024-11-25 09:13:44 +03:00
ef24796243
Fixes #497.
325 lines
13 KiB
Python
325 lines
13 KiB
Python
import collections
|
|
from urllib import parse
|
|
|
|
import http_strategies
|
|
from http_base import DaphneTestCase
|
|
from hypothesis import assume, given, settings
|
|
from hypothesis.strategies import integers
|
|
|
|
|
|
class TestHTTPRequest(DaphneTestCase):
|
|
"""
|
|
Tests the HTTP request handling.
|
|
"""
|
|
|
|
def assert_valid_http_scope(
|
|
self, scope, method, path, params=None, headers=None, scheme=None
|
|
):
|
|
"""
|
|
Checks that the passed scope is a valid ASGI HTTP scope regarding types
|
|
and some urlencoding things.
|
|
"""
|
|
# Check overall keys
|
|
self.assert_key_sets(
|
|
required_keys={
|
|
"asgi",
|
|
"type",
|
|
"http_version",
|
|
"method",
|
|
"path",
|
|
"raw_path",
|
|
"query_string",
|
|
"headers",
|
|
},
|
|
optional_keys={"scheme", "root_path", "client", "server"},
|
|
actual_keys=scope.keys(),
|
|
)
|
|
self.assertEqual(scope["asgi"]["version"], "3.0")
|
|
# Check that it is the right type
|
|
self.assertEqual(scope["type"], "http")
|
|
# Method (uppercased unicode string)
|
|
self.assertIsInstance(scope["method"], str)
|
|
self.assertEqual(scope["method"], method.upper())
|
|
# Path
|
|
self.assert_valid_path(scope["path"])
|
|
# HTTP version
|
|
self.assertIn(scope["http_version"], ["1.0", "1.1", "1.2"])
|
|
# Scheme
|
|
self.assertIn(scope["scheme"], ["http", "https"])
|
|
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:
|
|
self.assertEqual(
|
|
query_string, parse.urlencode(params or []).encode("ascii")
|
|
)
|
|
# 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"]:
|
|
transformed_scope_headers[name].append(value)
|
|
transformed_request_headers = collections.defaultdict(list)
|
|
for name, value in headers or []:
|
|
expected_name = name.lower().strip()
|
|
expected_value = value.strip()
|
|
transformed_request_headers[expected_name].append(expected_value)
|
|
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)
|
|
|
|
def assert_valid_http_request_message(self, message, body=None):
|
|
"""
|
|
Asserts that a message is a valid http.request message
|
|
"""
|
|
# Check overall keys
|
|
self.assert_key_sets(
|
|
required_keys={"type"},
|
|
optional_keys={"body", "more_body"},
|
|
actual_keys=message.keys(),
|
|
)
|
|
# Check that it is the right type
|
|
self.assertEqual(message["type"], "http.request")
|
|
# If there's a body present, check its type
|
|
self.assertIsInstance(message.get("body", b""), bytes)
|
|
if body is not None:
|
|
self.assertEqual(body, message.get("body", b""))
|
|
|
|
def test_minimal_request(self):
|
|
"""
|
|
Smallest viable example. Mostly verifies that our request building works.
|
|
"""
|
|
scope, messages = self.run_daphne_request("GET", "/")
|
|
self.assert_valid_http_scope(scope, "GET", "/")
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
|
|
@given(
|
|
request_path=http_strategies.http_path(),
|
|
request_params=http_strategies.query_params(),
|
|
)
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_get_request(self, request_path, request_params):
|
|
"""
|
|
Tests a typical HTTP GET request, with a path and query parameters
|
|
"""
|
|
scope, messages = self.run_daphne_request(
|
|
"GET", request_path, params=request_params
|
|
)
|
|
self.assert_valid_http_scope(scope, "GET", request_path, params=request_params)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
|
|
@given(request_path=http_strategies.http_path(), chunk_size=integers(min_value=1))
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_request_body_chunking(self, request_path, chunk_size):
|
|
"""
|
|
Tests request body chunking logic.
|
|
"""
|
|
body = b"The quick brown fox jumps over the lazy dog"
|
|
_, messages = self.run_daphne_request(
|
|
"POST",
|
|
request_path,
|
|
body=body,
|
|
request_buffer_size=chunk_size,
|
|
)
|
|
|
|
# Avoid running those asserts when there's a single "http.disconnect"
|
|
if len(messages) > 1:
|
|
assert messages[0]["body"].decode() == body.decode()[:chunk_size]
|
|
assert not messages[-2]["more_body"]
|
|
assert messages[-1] == {"type": "http.disconnect"}
|
|
|
|
@given(
|
|
request_path=http_strategies.http_path(),
|
|
request_body=http_strategies.http_body(),
|
|
)
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_post_request(self, request_path, request_body):
|
|
"""
|
|
Tests a typical HTTP POST request, with a path and body.
|
|
"""
|
|
scope, messages = self.run_daphne_request(
|
|
"POST", request_path, body=request_body
|
|
)
|
|
self.assert_valid_http_scope(scope, "POST", request_path)
|
|
self.assert_valid_http_request_message(messages[0], body=request_body)
|
|
|
|
def test_raw_path(self):
|
|
"""
|
|
Tests that /foo%2Fbar produces raw_path and a decoded path
|
|
"""
|
|
scope, _ = self.run_daphne_request("GET", "/foo%2Fbar")
|
|
self.assertEqual(scope["path"], "/foo/bar")
|
|
self.assertEqual(scope["raw_path"], b"/foo%2Fbar")
|
|
|
|
@given(request_headers=http_strategies.headers())
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_headers(self, request_headers):
|
|
"""
|
|
Tests that HTTP header fields are handled as specified
|
|
"""
|
|
request_path = parse.quote("/te st-à/")
|
|
scope, messages = self.run_daphne_request(
|
|
"OPTIONS", request_path, headers=request_headers
|
|
)
|
|
self.assert_valid_http_scope(
|
|
scope, "OPTIONS", request_path, headers=request_headers
|
|
)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
|
|
@given(request_headers=http_strategies.headers())
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_duplicate_headers(self, request_headers):
|
|
"""
|
|
Tests that duplicate header values are preserved
|
|
"""
|
|
# Make sure there's duplicate headers
|
|
assume(len(request_headers) >= 2)
|
|
header_name = request_headers[0][0]
|
|
duplicated_headers = [(header_name, header[1]) for header in request_headers]
|
|
# Run the request
|
|
request_path = parse.quote("/te st-à/")
|
|
scope, messages = self.run_daphne_request(
|
|
"OPTIONS", request_path, headers=duplicated_headers
|
|
)
|
|
self.assert_valid_http_scope(
|
|
scope, "OPTIONS", request_path, headers=duplicated_headers
|
|
)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
|
|
@given(
|
|
request_method=http_strategies.http_method(),
|
|
request_path=http_strategies.http_path(),
|
|
request_params=http_strategies.query_params(),
|
|
request_headers=http_strategies.headers(),
|
|
request_body=http_strategies.http_body(),
|
|
)
|
|
@settings(max_examples=2, deadline=5000)
|
|
def test_kitchen_sink(
|
|
self,
|
|
request_method,
|
|
request_path,
|
|
request_params,
|
|
request_headers,
|
|
request_body,
|
|
):
|
|
"""
|
|
Throw everything at Daphne that we dare. The idea is that if a combination
|
|
of method/path/headers/body would break the spec, hypothesis will eventually find it.
|
|
"""
|
|
scope, messages = self.run_daphne_request(
|
|
request_method,
|
|
request_path,
|
|
params=request_params,
|
|
headers=request_headers,
|
|
body=request_body,
|
|
)
|
|
self.assert_valid_http_scope(
|
|
scope,
|
|
request_method,
|
|
request_path,
|
|
params=request_params,
|
|
headers=request_headers,
|
|
)
|
|
self.assert_valid_http_request_message(messages[0], body=request_body)
|
|
|
|
def test_headers_are_lowercased_and_stripped(self):
|
|
"""
|
|
Make sure headers are normalized as the spec says they are.
|
|
"""
|
|
headers = [(b"MYCUSTOMHEADER", b" foobar ")]
|
|
scope, messages = self.run_daphne_request("GET", "/", headers=headers)
|
|
self.assert_valid_http_scope(scope, "GET", "/", headers=headers)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
# Note that Daphne returns a list of tuples here, which is fine, because the spec
|
|
# asks to treat them interchangeably.
|
|
assert [list(x) for x in scope["headers"]] == [[b"mycustomheader", b"foobar"]]
|
|
|
|
@given(daphne_path=http_strategies.http_path())
|
|
@settings(max_examples=5, deadline=5000)
|
|
def test_root_path_header(self, daphne_path):
|
|
"""
|
|
Tests root_path handling.
|
|
"""
|
|
# Daphne-Root-Path must be URL encoded when submitting as HTTP header field
|
|
headers = [("Daphne-Root-Path", parse.quote(daphne_path.encode("utf8")))]
|
|
scope, messages = self.run_daphne_request("GET", "/", headers=headers)
|
|
# Daphne-Root-Path is not included in the returned 'headers' section. So we expect
|
|
# empty headers.
|
|
self.assert_valid_http_scope(scope, "GET", "/", headers=[])
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
# And what we're looking for, root_path being set.
|
|
assert scope["root_path"] == daphne_path
|
|
|
|
def test_x_forwarded_for_ignored(self):
|
|
"""
|
|
Make sure that, by default, X-Forwarded-For is ignored.
|
|
"""
|
|
headers = [[b"X-Forwarded-For", b"10.1.2.3"], [b"X-Forwarded-Port", b"80"]]
|
|
scope, messages = self.run_daphne_request("GET", "/", headers=headers)
|
|
self.assert_valid_http_scope(scope, "GET", "/", headers=headers)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
# It should NOT appear in the client scope item
|
|
self.assertNotEqual(scope["client"], ["10.1.2.3", 80])
|
|
|
|
def test_x_forwarded_for_parsed(self):
|
|
"""
|
|
When X-Forwarded-For is enabled, make sure it is respected.
|
|
"""
|
|
headers = [[b"X-Forwarded-For", b"10.1.2.3"], [b"X-Forwarded-Port", b"80"]]
|
|
scope, messages = self.run_daphne_request("GET", "/", headers=headers, xff=True)
|
|
self.assert_valid_http_scope(scope, "GET", "/", headers=headers)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
# It should now appear in the client scope item
|
|
self.assertEqual(scope["client"], ["10.1.2.3", 80])
|
|
|
|
def test_x_forwarded_for_no_port(self):
|
|
"""
|
|
When X-Forwarded-For is enabled but only the host is passed, make sure
|
|
that at least makes it through.
|
|
"""
|
|
headers = [[b"X-Forwarded-For", b"10.1.2.3"]]
|
|
scope, messages = self.run_daphne_request("GET", "/", headers=headers, xff=True)
|
|
self.assert_valid_http_scope(scope, "GET", "/", headers=headers)
|
|
self.assert_valid_http_request_message(messages[0], body=b"")
|
|
# It should now appear in the client scope item
|
|
self.assertEqual(scope["client"], ["10.1.2.3", 0])
|
|
|
|
def test_bad_requests(self):
|
|
"""
|
|
Tests that requests with invalid (non-ASCII) characters fail.
|
|
"""
|
|
# Bad path
|
|
response = self.run_daphne_raw(
|
|
b"GET /\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
|
)
|
|
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
|
# Bad querystring
|
|
response = self.run_daphne_raw(
|
|
b"GET /?\xc3\xa4\xc3\xb6\xc3\xbc HTTP/1.0\r\n\r\n"
|
|
)
|
|
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|
|
|
|
def test_invalid_header_name(self):
|
|
"""
|
|
Tests that requests with invalid header names fail.
|
|
"""
|
|
# Test cases follow those used by h11
|
|
# https://github.com/python-hyper/h11/blob/a2c68948accadc3876dffcf979d98002e4a4ed27/h11/tests/test_headers.py#L24-L35
|
|
for header_name in [b"foo bar", b"foo\x00bar", b"foo\xffbar", b"foo\x01bar"]:
|
|
response = self.run_daphne_raw(
|
|
f"GET / HTTP/1.0\r\n{header_name}: baz\r\n\r\n".encode("ascii")
|
|
)
|
|
self.assertTrue(response.startswith(b"HTTP/1.0 400 Bad Request"))
|