From 605f0ea958f0474ece9d571a5e58f30584ad69d8 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 14 Nov 2024 10:22:22 +0000 Subject: [PATCH 1/8] fix: No more BytesWarnings Fixes #1236. This patch makes all header operations operate on `bytes` and converts all headers and values to bytes before operation. With a follow up patch to `hpack` it should also increase efficiency as currently, `hpack` casts everything to a `str` first before converting back to bytes: https://github.com/python-hyper/hpack/blob/02afcab28ca56eb5259904fd414baa89e9f50266/src/hpack/hpack.py#L150-L151 --- src/h2/connection.py | 3 +- src/h2/events.py | 8 +- src/h2/stream.py | 23 ++-- src/h2/utilities.py | 202 ++++++++++++++++----------------- test/test_invalid_headers.py | 60 +++------- test/test_utility_functions.py | 13 +-- tox.ini | 2 +- 7 files changed, 136 insertions(+), 175 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index 25251e20a..b62297666 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -33,7 +33,7 @@ from .frame_buffer import FrameBuffer from .settings import Settings, SettingCodes from .stream import H2Stream, StreamClosedBy -from .utilities import SizeLimitDict, guard_increment_window +from .utilities import SizeLimitDict, guard_increment_window, utf8_encode_headers from .windows import WindowManager @@ -976,6 +976,7 @@ def push_stream(self, stream_id, promised_stream_id, request_headers): ) self.streams[promised_stream_id] = new_stream + request_headers = utf8_encode_headers(request_headers) frames = stream.push_stream_in_band( promised_stream_id, request_headers, self.encoder ) diff --git a/src/h2/events.py b/src/h2/events.py index 66c3cff4a..20cbccf75 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -589,11 +589,9 @@ def __init__(self): self.field_value = None def __repr__(self): - return ( - "" % ( - self.origin.decode('utf-8', 'ignore'), - self.field_value.decode('utf-8', 'ignore'), - ) + return "" % ( + self.origin.decode("utf-8", "replace"), + self.field_value.decode("utf-8", "replace"), ) diff --git a/src/h2/stream.py b/src/h2/stream.py index 1c34dcd3e..3e92596ff 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -23,9 +23,16 @@ ProtocolError, StreamClosedError, InvalidBodyLengthError, FlowControlError ) from .utilities import ( - guard_increment_window, is_informational_response, authority_from_headers, - validate_headers, validate_outbound_headers, normalize_outbound_headers, - HeaderValidationFlags, extract_method_header, normalize_inbound_headers + guard_increment_window, + is_informational_response, + authority_from_headers, + utf8_encode_headers, + validate_headers, + validate_outbound_headers, + normalize_outbound_headers, + HeaderValidationFlags, + extract_method_header, + normalize_inbound_headers, ) from .windows import WindowManager @@ -851,8 +858,9 @@ def send_headers(self, headers, encoder, end_stream=False): # we need to scan the header block to see if this is an informational # response. input_ = StreamInputs.SEND_HEADERS - if ((not self.state_machine.client) and - is_informational_response(headers)): + + headers = utf8_encode_headers(headers) + if (not self.state_machine.client) and is_informational_response(headers): if end_stream: raise ProtocolError( "Cannot set END_STREAM on informational responses." @@ -1242,6 +1250,7 @@ def _build_headers_frames(self, """ # We need to lowercase the header names, and to ensure that secure # header fields are kept out of compression contexts. + if self.config.normalize_outbound_headers: # also we may want to split outbound cookies to improve # headers compression @@ -1318,9 +1327,7 @@ def _initialize_content_length(self, headers): try: self._expected_content_length = int(v, 10) except ValueError: - raise ProtocolError( - "Invalid content-length header: %s" % v - ) + raise ProtocolError(f"Invalid content-length header: {repr(v)}") return diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 3a7bf6e07..5cd1d1e8e 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -15,49 +15,60 @@ UPPER_RE = re.compile(b"[A-Z]") +SIGIL = ord(b":") + + # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. -CONNECTION_HEADERS = frozenset([ - b'connection', u'connection', - b'proxy-connection', u'proxy-connection', - b'keep-alive', u'keep-alive', - b'transfer-encoding', u'transfer-encoding', - b'upgrade', u'upgrade', -]) +CONNECTION_HEADERS = frozenset( + [ + b"connection", + b"proxy-connection", + b"keep-alive", + b"transfer-encoding", + b"upgrade", + ] +) -_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([ - b':method', u':method', - b':scheme', u':scheme', - b':authority', u':authority', - b':path', u':path', - b':status', u':status', - b':protocol', u':protocol', -]) +_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset( + [ + b":method", + b":scheme", + b":authority", + b":path", + b":status", + b":protocol", + ] +) -_SECURE_HEADERS = frozenset([ - # May have basic credentials which are vulnerable to dictionary attacks. - b'authorization', u'authorization', - b'proxy-authorization', u'proxy-authorization', -]) +_SECURE_HEADERS = frozenset( + [ + # May have basic credentials which are vulnerable to dictionary attacks. + b"authorization", + b"proxy-authorization", + ] +) -_REQUEST_ONLY_HEADERS = frozenset([ - b':scheme', u':scheme', - b':path', u':path', - b':authority', u':authority', - b':method', u':method', - b':protocol', u':protocol', -]) +_REQUEST_ONLY_HEADERS = frozenset( + [ + b":scheme", + b":path", + b":authority", + b":method", + b":protocol", + ] +) -_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status']) +_RESPONSE_ONLY_HEADERS = frozenset([b":status"]) # A Set of pseudo headers that are only valid if the method is # CONNECT, see RFC 8441 § 5 -_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol', u':protocol']) +_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b":protocol"]) _WHITESPACE = frozenset(map(ord, whitespace)) @@ -84,7 +95,7 @@ def _secure_headers(headers, hdr_validation_flags): for header in headers: if header[0] in _SECURE_HEADERS: yield NeverIndexedHeaderTuple(*header) - elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20: + elif header[0] == b"cookie" and len(header[1]) < 20: yield NeverIndexedHeaderTuple(*header) else: yield header @@ -95,11 +106,8 @@ def extract_method_header(headers): Extracts the request method from the headers list. """ for k, v in headers: - if k in (b':method', u':method'): - if not isinstance(v, bytes): - return v.encode('utf-8') - else: - return v + if k == b":method": + return v def is_informational_response(headers): @@ -113,18 +121,12 @@ def is_informational_response(headers): :param headers: The HTTP/2 header block. :returns: A boolean indicating if this is an informational response. """ + status = b":status" + informational_start = ord(b"1") for n, v in headers: - if isinstance(n, bytes): - sigil = b':' - status = b':status' - informational_start = b'1' - else: - sigil = u':' - status = u':status' - informational_start = u'1' - # If we find a non-special header, we're done here: stop looping. - if not n.startswith(sigil): + + if n and n[0] != SIGIL: return False # This isn't the status header, bail. @@ -132,7 +134,7 @@ def is_informational_response(headers): continue # If the first digit is a 1, we've got informational headers. - return v.startswith(informational_start) + return v[0] == informational_start def guard_increment_window(current, increment): @@ -164,7 +166,7 @@ def authority_from_headers(headers): Given a header set, searches for the authority header and returns the value. - Note that this doesn't terminate early, so should only be called if the + Note that this doesn't use indexing, so should only be called if the headers are for a client request. Otherwise, will loop over the entire header set, which is potentially unwise. @@ -173,11 +175,8 @@ def authority_from_headers(headers): :rtype: ``bytes`` or ``None``. """ for n, v in headers: - # This gets run against headers that come both from HPACK and from the - # user, so we may have unicode floating around in here. We only want - # bytes. - if n in (b':authority', u':authority'): - return v.encode('utf-8') if not isinstance(v, bytes) else v + if n == b":authority": + return v return None @@ -252,8 +251,7 @@ def _reject_uppercase_header_fields(headers, hdr_validation_flags): """ for header in headers: if UPPER_RE.search(header[0]): - raise ProtocolError( - "Received uppercase header name %s." % header[0]) + raise ProtocolError(f"Received uppercase header name {repr(header[0])}.") yield header @@ -285,12 +283,9 @@ def _reject_te(headers, hdr_validation_flags): its value is anything other than "trailers". """ for header in headers: - if header[0] in (b'te', u'te'): - if header[1].lower() not in (b'trailers', u'trailers'): - raise ProtocolError( - "Invalid value for TE header: %s" % - header[1] - ) + if header[0] == b"te": + if header[1].lower() != b"trailers": + raise ProtocolError(f"Invalid value for TE header: {repr(header[1])}") yield header @@ -303,32 +298,21 @@ def _reject_connection_header(headers, hdr_validation_flags): for header in headers: if header[0] in CONNECTION_HEADERS: raise ProtocolError( - "Connection-specific header field present: %s." % header[0] + f"Connection-specific header field present: {repr(header[0])}." ) yield header -def _custom_startswith(test_string, bytes_prefix, unicode_prefix): - """ - Given a string that might be a bytestring or a Unicode string, - return True if it starts with the appropriate prefix. - """ - if isinstance(test_string, bytes): - return test_string.startswith(bytes_prefix) - else: - return test_string.startswith(unicode_prefix) - - -def _assert_header_in_set(string_header, bytes_header, header_set): +def _assert_header_in_set(bytes_header, header_set): """ Given a set of header names, checks whether the string or byte version of the header name is present. Raises a Protocol error with the appropriate error if it's missing. """ - if not (string_header in header_set or bytes_header in header_set): + if bytes_header not in header_set: raise ProtocolError( - "Header block missing mandatory %s header" % string_header + f"Header block missing mandatory {repr(bytes_header)} header" ) @@ -345,30 +329,26 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags): method = None for header in headers: - if _custom_startswith(header[0], b':', u':'): + if header[0][0] == SIGIL: if header[0] in seen_pseudo_header_fields: raise ProtocolError( - "Received duplicate pseudo-header field %s" % header[0] + f"Received duplicate pseudo-header field {repr(header[0])}" ) seen_pseudo_header_fields.add(header[0]) if seen_regular_header: raise ProtocolError( - "Received pseudo-header field out of sequence: %s" % - header[0] + f"Received pseudo-header field out of sequence: {repr(header[0])}" ) if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS: raise ProtocolError( - "Received custom pseudo-header field %s" % header[0] + f"Received custom pseudo-header field {repr(header[0])}" ) - if header[0] in (b':method', u':method'): - if not isinstance(header[1], bytes): - method = header[1].encode('utf-8') - else: - method = header[1] + if header[0] in b":method": + method = header[1] else: seen_regular_header = True @@ -401,7 +381,7 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, # Relevant RFC section: RFC 7540 § 8.1.2.4 # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 if hdr_validation_flags.is_response_header: - _assert_header_in_set(u':status', b':status', pseudo_headers) + _assert_header_in_set(b":status", pseudo_headers) invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS if invalid_response_headers: raise ProtocolError( @@ -412,9 +392,9 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, not hdr_validation_flags.is_trailer): # This is a request, so we need to have seen :path, :method, and # :scheme. - _assert_header_in_set(u':path', b':path', pseudo_headers) - _assert_header_in_set(u':method', b':method', pseudo_headers) - _assert_header_in_set(u':scheme', b':scheme', pseudo_headers) + _assert_header_in_set(b":path", pseudo_headers) + _assert_header_in_set(b":method", pseudo_headers) + _assert_header_in_set(b":scheme", pseudo_headers) invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS if invalid_request_headers: raise ProtocolError( @@ -451,9 +431,9 @@ def _validate_host_authority_header(headers): host_header_val = None for header in headers: - if header[0] in (b':authority', u':authority'): + if header[0] == b":authority": authority_header_val = header[1] - elif header[0] in (b'host', u'host'): + elif header[0] == b"host": host_header_val = header[1] yield header @@ -506,7 +486,7 @@ def _check_path_header(headers, hdr_validation_flags): """ def inner(): for header in headers: - if header[0] in (b':path', u':path'): + if header[0] == b":path": if not header[1]: raise ProtocolError("An empty :path header is forbidden") @@ -525,6 +505,27 @@ def inner(): return inner() +def _to_bytes(v): + return v if isinstance(v, bytes) else v.encode("utf-8") + + +def utf8_encode_headers(headers): + """ + Given an iterable of header two-tuples, rebuilds that iterable with the + header names and values encoded as utf-8 bytes. This generator produces + tuples that preserve the original type of the header tuple for tuple and + any ``HeaderTuple``. + """ + return [ + ( + header.__class__(_to_bytes(header[0]), _to_bytes(header[1])) + if isinstance(header, HeaderTuple) + else (_to_bytes(header[0]), _to_bytes(header[1])) + ) + for header in headers + ] + + def _lowercase_header_names(headers, hdr_validation_flags): """ Given an iterable of header two-tuples, rebuilds that iterable with the @@ -612,17 +613,12 @@ def _split_outbound_cookie_fields(headers, hdr_validation_flags): inbound. """ for header in headers: - if header[0] in (b'cookie', 'cookie'): - needle = b'; ' if isinstance(header[0], bytes) else '; ' - - if needle in header[1]: - for cookie_val in header[1].split(needle): - if isinstance(header, HeaderTuple): - yield header.__class__(header[0], cookie_val) - else: - yield header[0], cookie_val - else: - yield header + if header[0] == b"cookie": + for cookie_val in header[1].split(b"; "): + if isinstance(header, HeaderTuple): + yield header.__class__(header[0], cookie_val) + else: + yield header[0], cookie_val else: yield header diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 165183e28..93781814d 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -296,7 +296,9 @@ def test_headers_event_skipping_validation(self, frame_factory, headers): c.send_headers(1, headers) # Ensure headers are still normalized. - norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False) + norm_headers = h2.utilities.normalize_outbound_headers( + h2.utilities.utf8_encode_headers(headers), None, False + ) f = frame_factory.build_headers_frame(norm_headers) assert c.data_to_send() == f.serialize() @@ -322,7 +324,9 @@ def test_push_promise_skipping_validation(self, frame_factory, headers): # Create push promise frame with normalized headers. frame_factory.refresh_encoder() - norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False) + norm_headers = h2.utilities.normalize_outbound_headers( + h2.utilities.utf8_encode_headers(headers), None, False + ) pp_frame = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, headers=norm_headers ) @@ -467,43 +471,10 @@ class TestFilter(object): (b':path', b''), ), ) - invalid_request_header_blocks_unicode = ( - # First, missing :method - ( - (':authority', 'google.com'), - (':path', '/'), - (':scheme', 'https'), - ), - # Next, missing :path - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':scheme', 'https'), - ), - # Next, missing :scheme - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':path', '/'), - ), - # Finally, path present but empty. - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':scheme', 'https'), - (':path', ''), - ), - ) # All headers that are forbidden from either request or response blocks. - forbidden_request_headers_bytes = (b':status',) - forbidden_request_headers_unicode = (':status',) - forbidden_response_headers_bytes = ( - b':path', b':scheme', b':authority', b':method' - ) - forbidden_response_headers_unicode = ( - ':path', ':scheme', ':authority', ':method' - ) + forbidden_request_headers_bytes = (b":status",) + forbidden_response_headers_bytes = (b":path", b":scheme", b":authority", b":method") @pytest.mark.parametrize('validation_function', validation_functions) @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos) @@ -562,11 +533,8 @@ def test_response_header_without_status(self, hdr_validation_flags): 'hdr_validation_flags', hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - 'header_block', - ( - invalid_request_header_blocks_bytes + - invalid_request_header_blocks_unicode - ) + "header_block", + (invalid_request_header_blocks_bytes), ) def test_outbound_req_header_missing_pseudo_headers(self, hdr_validation_flags, @@ -598,8 +566,8 @@ def test_inbound_req_header_missing_pseudo_headers(self, 'hdr_validation_flags', hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - 'invalid_header', - forbidden_request_headers_bytes + forbidden_request_headers_unicode + "invalid_header", + forbidden_request_headers_bytes, ) def test_outbound_req_header_extra_pseudo_headers(self, hdr_validation_flags, @@ -650,8 +618,8 @@ def test_inbound_req_header_extra_pseudo_headers(self, 'hdr_validation_flags', hdr_validation_response_headers ) @pytest.mark.parametrize( - 'invalid_header', - forbidden_response_headers_bytes + forbidden_response_headers_unicode + "invalid_header", + forbidden_response_headers_bytes, ) def test_outbound_resp_header_extra_pseudo_headers(self, hdr_validation_flags, diff --git a/test/test_utility_functions.py b/test/test_utility_functions.py index c6578df35..a904bcfac 100644 --- a/test/test_utility_functions.py +++ b/test/test_utility_functions.py @@ -152,12 +152,6 @@ def test_does_not_increment_without_stream_send(self): class TestExtractHeader(object): - example_request_headers = [ - (u':authority', u'example.com'), - (u':path', u'/'), - (u':scheme', u'https'), - (u':method', u'GET'), - ] example_headers_with_bytes = [ (b':authority', b'example.com'), (b':path', b'/'), @@ -165,11 +159,8 @@ class TestExtractHeader(object): (b':method', b'GET'), ] - @pytest.mark.parametrize( - 'headers', [example_request_headers, example_headers_with_bytes] - ) - def test_extract_header_method(self, headers): - assert extract_method_header(headers) == b'GET' + def test_extract_header_method(self): + assert extract_method_header(self.example_headers_with_bytes) == b"GET" def test_size_limit_dict_limit(): diff --git a/tox.ini b/tox.ini index eaf0a4360..6d8ddc563 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = pytest-xdist>=2.0.0,<3 hypothesis>=5.5,<7 commands = - pytest --cov-report=xml --cov-report=term --cov=h2 {posargs} + python -bb -m pytest --cov-report=xml --cov-report=term --cov=h2 {posargs} [testenv:pypy3] # temporarily disable coverage testing on PyPy due to performance problems From 716c67e20782b7a3e2b14837f3badf564b9a33e5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 14 Nov 2024 10:37:49 +0000 Subject: [PATCH 2/8] some docstring fixes --- src/h2/utilities.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 5cd1d1e8e..ba048f5fa 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -506,13 +506,18 @@ def inner(): def _to_bytes(v): + """ + Given an assumed `str` (or anything that supports `.encode()`), + encodes it using utf-8 into bytes. Returns the unmodified object + if it is already a `bytes` object. + """ return v if isinstance(v, bytes) else v.encode("utf-8") def utf8_encode_headers(headers): """ - Given an iterable of header two-tuples, rebuilds that iterable with the - header names and values encoded as utf-8 bytes. This generator produces + Given an iterable of header two-tuples, rebuilds that as a list with the + header names and values encoded as utf-8 bytes. This function produces tuples that preserve the original type of the header tuple for tuple and any ``HeaderTuple``. """ From e36eeff76d44e31982c7f27cfdb4106e7e9f2add Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 14 Nov 2024 21:22:25 +0000 Subject: [PATCH 3/8] revert moar stylistic changes --- src/h2/stream.py | 21 +++++------ src/h2/utilities.py | 66 ++++++++++++++++------------------ test/test_invalid_headers.py | 10 +++--- test/test_utility_functions.py | 4 ++- 4 files changed, 47 insertions(+), 54 deletions(-) diff --git a/src/h2/stream.py b/src/h2/stream.py index 3e92596ff..778f0856d 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -23,16 +23,10 @@ ProtocolError, StreamClosedError, InvalidBodyLengthError, FlowControlError ) from .utilities import ( - guard_increment_window, - is_informational_response, - authority_from_headers, - utf8_encode_headers, - validate_headers, - validate_outbound_headers, - normalize_outbound_headers, - HeaderValidationFlags, - extract_method_header, - normalize_inbound_headers, + guard_increment_window, is_informational_response, authority_from_headers, + validate_headers, validate_outbound_headers, normalize_outbound_headers, + HeaderValidationFlags, extract_method_header, normalize_inbound_headers, + utf8_encode_headers ) from .windows import WindowManager @@ -860,7 +854,8 @@ def send_headers(self, headers, encoder, end_stream=False): input_ = StreamInputs.SEND_HEADERS headers = utf8_encode_headers(headers) - if (not self.state_machine.client) and is_informational_response(headers): + if ((not self.state_machine.client) and + is_informational_response(headers)): if end_stream: raise ProtocolError( "Cannot set END_STREAM on informational responses." @@ -1327,7 +1322,9 @@ def _initialize_content_length(self, headers): try: self._expected_content_length = int(v, 10) except ValueError: - raise ProtocolError(f"Invalid content-length header: {repr(v)}") + raise ProtocolError( + f"Invalid content-length header: {repr(v)}" + ) return diff --git a/src/h2/utilities.py b/src/h2/utilities.py index ba048f5fa..dc1e1abc7 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -20,47 +20,39 @@ # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. -CONNECTION_HEADERS = frozenset( - [ - b"connection", - b"proxy-connection", - b"keep-alive", - b"transfer-encoding", - b"upgrade", - ] -) +CONNECTION_HEADERS = frozenset([ + b"connection", + b"proxy-connection", + b"keep-alive", + b"transfer-encoding", + b"upgrade", +]) -_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset( - [ - b":method", - b":scheme", - b":authority", - b":path", - b":status", - b":protocol", - ] -) +_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([ + b":method", + b":scheme", + b":authority", + b":path", + b":status", + b":protocol", +]) -_SECURE_HEADERS = frozenset( - [ - # May have basic credentials which are vulnerable to dictionary attacks. - b"authorization", - b"proxy-authorization", - ] -) +_SECURE_HEADERS = frozenset([ + # May have basic credentials which are vulnerable to dictionary attacks. + b"authorization", + b"proxy-authorization", +]) -_REQUEST_ONLY_HEADERS = frozenset( - [ - b":scheme", - b":path", - b":authority", - b":method", - b":protocol", - ] -) +_REQUEST_ONLY_HEADERS = frozenset([ + b":scheme", + b":path", + b":authority", + b":method", + b":protocol", +]) _RESPONSE_ONLY_HEADERS = frozenset([b":status"]) @@ -285,7 +277,9 @@ def _reject_te(headers, hdr_validation_flags): for header in headers: if header[0] == b"te": if header[1].lower() != b"trailers": - raise ProtocolError(f"Invalid value for TE header: {repr(header[1])}") + raise ProtocolError( + f"Invalid value for TE header: {repr(header[1])}" + ) yield header diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 93781814d..2690d3140 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -473,8 +473,8 @@ class TestFilter(object): ) # All headers that are forbidden from either request or response blocks. - forbidden_request_headers_bytes = (b":status",) - forbidden_response_headers_bytes = (b":path", b":scheme", b":authority", b":method") + forbidden_request_headers_bytes = (b':status',) + forbidden_response_headers_bytes = (b':path', b':scheme', b':authority', b':method') @pytest.mark.parametrize('validation_function', validation_functions) @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos) @@ -533,7 +533,7 @@ def test_response_header_without_status(self, hdr_validation_flags): 'hdr_validation_flags', hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - "header_block", + 'header_block', (invalid_request_header_blocks_bytes), ) def test_outbound_req_header_missing_pseudo_headers(self, @@ -566,7 +566,7 @@ def test_inbound_req_header_missing_pseudo_headers(self, 'hdr_validation_flags', hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - "invalid_header", + 'invalid_header', forbidden_request_headers_bytes, ) def test_outbound_req_header_extra_pseudo_headers(self, @@ -618,7 +618,7 @@ def test_inbound_req_header_extra_pseudo_headers(self, 'hdr_validation_flags', hdr_validation_response_headers ) @pytest.mark.parametrize( - "invalid_header", + 'invalid_header', forbidden_response_headers_bytes, ) def test_outbound_resp_header_extra_pseudo_headers(self, diff --git a/test/test_utility_functions.py b/test/test_utility_functions.py index a904bcfac..3aa0a2452 100644 --- a/test/test_utility_functions.py +++ b/test/test_utility_functions.py @@ -160,7 +160,9 @@ class TestExtractHeader(object): ] def test_extract_header_method(self): - assert extract_method_header(self.example_headers_with_bytes) == b"GET" + assert extract_method_header( + self.example_headers_with_bytes + ) == b'GET' def test_size_limit_dict_limit(): From d0849ca3df7f9fadd784424e2ea5f60d18e49a95 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 14 Nov 2024 21:32:47 +0000 Subject: [PATCH 4/8] revert even moar stylistic changes --- src/h2/stream.py | 1 - src/h2/utilities.py | 91 +++++++++++++++++++++++---------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/h2/stream.py b/src/h2/stream.py index 778f0856d..629bbe548 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -1245,7 +1245,6 @@ def _build_headers_frames(self, """ # We need to lowercase the header names, and to ensure that secure # header fields are kept out of compression contexts. - if self.config.normalize_outbound_headers: # also we may want to split outbound cookies to improve # headers compression diff --git a/src/h2/utilities.py b/src/h2/utilities.py index dc1e1abc7..25951ac41 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -13,54 +13,56 @@ from .exceptions import ProtocolError, FlowControlError -UPPER_RE = re.compile(b"[A-Z]") -SIGIL = ord(b":") +UPPER_RE = re.compile(b"[A-Z]") +SIGIL = ord(b':') +STATUS_HEADER = b':status' +INFORMATIONAL_START = ord(b'1') # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. CONNECTION_HEADERS = frozenset([ - b"connection", - b"proxy-connection", - b"keep-alive", - b"transfer-encoding", - b"upgrade", + b'connection', + b'proxy-connection', + b'keep-alive', + b'transfer-encoding', + b'upgrade', ]) _ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([ - b":method", - b":scheme", - b":authority", - b":path", - b":status", - b":protocol", + b':method', + b':scheme', + b':authority', + b':path', + b':status', + b':protocol', ]) _SECURE_HEADERS = frozenset([ # May have basic credentials which are vulnerable to dictionary attacks. - b"authorization", - b"proxy-authorization", + b'authorization', + b'proxy-authorization', ]) _REQUEST_ONLY_HEADERS = frozenset([ - b":scheme", - b":path", - b":authority", - b":method", - b":protocol", + b':scheme', + b':path', + b':authority', + b':method', + b':protocol', ]) -_RESPONSE_ONLY_HEADERS = frozenset([b":status"]) +_RESPONSE_ONLY_HEADERS = frozenset([b':status']) # A Set of pseudo headers that are only valid if the method is # CONNECT, see RFC 8441 § 5 -_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b":protocol"]) +_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol']) _WHITESPACE = frozenset(map(ord, whitespace)) @@ -87,7 +89,7 @@ def _secure_headers(headers, hdr_validation_flags): for header in headers: if header[0] in _SECURE_HEADERS: yield NeverIndexedHeaderTuple(*header) - elif header[0] == b"cookie" and len(header[1]) < 20: + elif header[0] == b'cookie' and len(header[1]) < 20: yield NeverIndexedHeaderTuple(*header) else: yield header @@ -98,7 +100,7 @@ def extract_method_header(headers): Extracts the request method from the headers list. """ for k, v in headers: - if k == b":method": + if k == b':method': return v @@ -113,8 +115,6 @@ def is_informational_response(headers): :param headers: The HTTP/2 header block. :returns: A boolean indicating if this is an informational response. """ - status = b":status" - informational_start = ord(b"1") for n, v in headers: # If we find a non-special header, we're done here: stop looping. @@ -122,11 +122,11 @@ def is_informational_response(headers): return False # This isn't the status header, bail. - if n != status: + if n != STATUS_HEADER: continue # If the first digit is a 1, we've got informational headers. - return v[0] == informational_start + return v[0] == INFORMATIONAL_START def guard_increment_window(current, increment): @@ -167,7 +167,7 @@ def authority_from_headers(headers): :rtype: ``bytes`` or ``None``. """ for n, v in headers: - if n == b":authority": + if n == b':authority': return v return None @@ -243,7 +243,9 @@ def _reject_uppercase_header_fields(headers, hdr_validation_flags): """ for header in headers: if UPPER_RE.search(header[0]): - raise ProtocolError(f"Received uppercase header name {repr(header[0])}.") + raise ProtocolError( + f"Received uppercase header name {repr(header[0])}." + ) yield header @@ -275,8 +277,8 @@ def _reject_te(headers, hdr_validation_flags): its value is anything other than "trailers". """ for header in headers: - if header[0] == b"te": - if header[1].lower() != b"trailers": + if header[0] == b'te': + if header[1].lower() != b'trailers': raise ProtocolError( f"Invalid value for TE header: {repr(header[1])}" ) @@ -341,7 +343,7 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags): f"Received custom pseudo-header field {repr(header[0])}" ) - if header[0] in b":method": + if header[0] in b':method': method = header[1] else: @@ -375,7 +377,7 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, # Relevant RFC section: RFC 7540 § 8.1.2.4 # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 if hdr_validation_flags.is_response_header: - _assert_header_in_set(b":status", pseudo_headers) + _assert_header_in_set(STATUS_HEADER, pseudo_headers) invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS if invalid_response_headers: raise ProtocolError( @@ -386,9 +388,9 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, not hdr_validation_flags.is_trailer): # This is a request, so we need to have seen :path, :method, and # :scheme. - _assert_header_in_set(b":path", pseudo_headers) - _assert_header_in_set(b":method", pseudo_headers) - _assert_header_in_set(b":scheme", pseudo_headers) + _assert_header_in_set(b':path', pseudo_headers) + _assert_header_in_set(b':method', pseudo_headers) + _assert_header_in_set(b':scheme', pseudo_headers) invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS if invalid_request_headers: raise ProtocolError( @@ -399,8 +401,7 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS if invalid_headers: raise ProtocolError( - "Encountered connect-request-only headers %s" % - invalid_headers + f"Encountered connect-request-only headers {repr(invalid_headers)}" ) @@ -425,9 +426,9 @@ def _validate_host_authority_header(headers): host_header_val = None for header in headers: - if header[0] == b":authority": + if header[0] == b':authority': authority_header_val = header[1] - elif header[0] == b"host": + elif header[0] == b'host': host_header_val = header[1] yield header @@ -480,7 +481,7 @@ def _check_path_header(headers, hdr_validation_flags): """ def inner(): for header in headers: - if header[0] == b":path": + if header[0] == b':path': if not header[1]: raise ProtocolError("An empty :path header is forbidden") @@ -505,7 +506,7 @@ def _to_bytes(v): encodes it using utf-8 into bytes. Returns the unmodified object if it is already a `bytes` object. """ - return v if isinstance(v, bytes) else v.encode("utf-8") + return v if isinstance(v, bytes) else v.encode('utf-8') def utf8_encode_headers(headers): @@ -612,8 +613,8 @@ def _split_outbound_cookie_fields(headers, hdr_validation_flags): inbound. """ for header in headers: - if header[0] == b"cookie": - for cookie_val in header[1].split(b"; "): + if header[0] == b'cookie': + for cookie_val in header[1].split(b'; '): if isinstance(header, HeaderTuple): yield header.__class__(header[0], cookie_val) else: From 027e04c731ac8c54eeabf1360e5bf8525fd2cd63 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 15 Nov 2024 10:22:03 +0000 Subject: [PATCH 5/8] undo STATUS_HEADER --- src/h2/utilities.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 25951ac41..f373a2275 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -16,7 +16,6 @@ UPPER_RE = re.compile(b"[A-Z]") SIGIL = ord(b':') -STATUS_HEADER = b':status' INFORMATIONAL_START = ord(b'1') @@ -122,7 +121,7 @@ def is_informational_response(headers): return False # This isn't the status header, bail. - if n != STATUS_HEADER: + if n != b'status': continue # If the first digit is a 1, we've got informational headers. @@ -377,7 +376,7 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, # Relevant RFC section: RFC 7540 § 8.1.2.4 # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 if hdr_validation_flags.is_response_header: - _assert_header_in_set(STATUS_HEADER, pseudo_headers) + _assert_header_in_set(b'status', pseudo_headers) invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS if invalid_response_headers: raise ProtocolError( From 913b893e87eda57176de62ab4c9cf438acec6f77 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 15 Nov 2024 10:29:36 +0000 Subject: [PATCH 6/8] fix bad refactor --- src/h2/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/h2/utilities.py b/src/h2/utilities.py index f373a2275..54cd6f210 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -121,7 +121,7 @@ def is_informational_response(headers): return False # This isn't the status header, bail. - if n != b'status': + if n != b':status': continue # If the first digit is a 1, we've got informational headers. @@ -376,7 +376,7 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, # Relevant RFC section: RFC 7540 § 8.1.2.4 # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 if hdr_validation_flags.is_response_header: - _assert_header_in_set(b'status', pseudo_headers) + _assert_header_in_set(b':status', pseudo_headers) invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS if invalid_response_headers: raise ProtocolError( From 66d43932172fd30e9e56b5a0a5b34c6ea3c5612b Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 15 Nov 2024 11:29:03 +0000 Subject: [PATCH 7/8] add more upstream tests --- test/test_h2_upgrade.py | 50 +++++++------ test/test_head_request.py | 32 +++++--- test/test_header_indexing.py | 106 +++++++++++++-------------- test/test_informational_responses.py | 55 ++++++++++---- test/test_interacting_stacks.py | 46 +++++++----- test/test_invalid_content_lengths.py | 22 ++++-- test/test_invalid_frame_sequences.py | 98 +++++++++++++++---------- test/test_invalid_headers.py | 11 +-- test/test_priority.py | 62 ++++++++++------ test/test_related_events.py | 74 ++++++++++++------- test/test_rfc7838.py | 74 ++++++++++++------- test/test_rfc8441.py | 33 ++++++--- 12 files changed, 412 insertions(+), 251 deletions(-) diff --git a/test/test_h2_upgrade.py b/test/test_h2_upgrade.py index d63d44f3f..1fa851e90 100644 --- a/test/test_h2_upgrade.py +++ b/test/test_h2_upgrade.py @@ -9,6 +9,7 @@ """ import base64 +from h2.utilities import utf8_encode_headers import pytest import h2.config @@ -18,16 +19,23 @@ import h2.exceptions +EXAMPLE_REQUEST_HEADERS = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'GET'), +] +EXAMPLE_REQUEST_HEADERS_BYTES = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), +] + class TestClientUpgrade(object): """ Tests of the client-side of the HTTP/2 upgrade dance. """ - example_request_headers = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'GET'), - ] example_response_headers = [ (b':status', b'200'), (b'server', b'fake-serv/0.1.0') @@ -97,7 +105,8 @@ def test_can_receive_response(self, frame_factory): assert not c.data_to_send() - def test_can_receive_pushed_stream(self, frame_factory): + @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_can_receive_pushed_stream(self, frame_factory, headers): """ After upgrading, we can safely receive a pushed stream. """ @@ -108,17 +117,18 @@ def test_can_receive_pushed_stream(self, frame_factory): f = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, - headers=self.example_request_headers, + headers=headers ) events = c.receive_data(f.serialize()) assert len(events) == 1 assert isinstance(events[0], h2.events.PushedStreamReceived) - assert events[0].headers == self.example_request_headers + assert events[0].headers == utf8_encode_headers(headers) assert events[0].parent_stream_id == 1 assert events[0].pushed_stream_id == 2 - def test_cannot_send_headers_stream_1(self, frame_factory): + @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_cannot_send_headers_stream_1(self, frame_factory, headers): """ After upgrading, we cannot send headers on stream 1. """ @@ -127,7 +137,7 @@ def test_cannot_send_headers_stream_1(self, frame_factory): c.clear_outbound_data_buffer() with pytest.raises(h2.exceptions.ProtocolError): - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=headers) def test_cannot_send_data_stream_1(self, frame_factory): """ @@ -145,12 +155,6 @@ class TestServerUpgrade(object): """ Tests of the server-side of the HTTP/2 upgrade dance. """ - example_request_headers = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'GET'), - ] example_response_headers = [ (b':status', b'200'), (b'server', b'fake-serv/0.1.0') @@ -203,7 +207,8 @@ def test_can_send_response(self, frame_factory): expected_data = f1.serialize() + f2.serialize() assert c.data_to_send() == expected_data - def test_can_push_stream(self, frame_factory): + @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_can_push_stream(self, frame_factory, headers): """ After upgrading, we can safely push a stream. """ @@ -214,17 +219,18 @@ def test_can_push_stream(self, frame_factory): c.push_stream( stream_id=1, promised_stream_id=2, - request_headers=self.example_request_headers + request_headers=headers ) f = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, - headers=self.example_request_headers, + headers=headers, ) assert c.data_to_send() == f.serialize() - def test_cannot_receive_headers_stream_1(self, frame_factory): + @pytest.mark.parametrize("headers", [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_cannot_receive_headers_stream_1(self, frame_factory, headers): """ After upgrading, we cannot receive headers on stream 1. """ @@ -235,7 +241,7 @@ def test_cannot_receive_headers_stream_1(self, frame_factory): f = frame_factory.build_headers_frame( stream_id=1, - headers=self.example_request_headers, + headers=headers, ) c.receive_data(f.serialize()) diff --git a/test/test_head_request.py b/test/test_head_request.py index ef7300725..a804f90a7 100644 --- a/test/test_head_request.py +++ b/test/test_head_request.py @@ -7,25 +7,32 @@ import pytest -class TestHeadRequest(object): - example_request_headers = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'HEAD'), - ] +EXAMPLE_REQUEST_HEADERS_BYTES = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'HEAD'), +] + +EXAMPLE_REQUEST_HEADERS = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'HEAD'), +] +class TestHeadRequest(object): example_response_headers = [ (b':status', b'200'), (b'server', b'fake-serv/0.1.0'), (b'content_length', b'1'), ] - def test_non_zero_content_and_no_body(self, frame_factory): - + @pytest.mark.parametrize('headers', [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_non_zero_content_and_no_body(self, frame_factory, headers): c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(1, self.example_request_headers, end_stream=True) + c.send_headers(1, headers, end_stream=True) f = frame_factory.build_headers_frame( self.example_response_headers, @@ -40,10 +47,11 @@ def test_non_zero_content_and_no_body(self, frame_factory): assert event.stream_id == 1 assert event.headers == self.example_response_headers - def test_reject_non_zero_content_and_body(self, frame_factory): + @pytest.mark.parametrize('headers', [EXAMPLE_REQUEST_HEADERS, EXAMPLE_REQUEST_HEADERS_BYTES]) + def test_reject_non_zero_content_and_body(self, frame_factory, headers): c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(1, self.example_request_headers) + c.send_headers(1, headers) headers = frame_factory.build_headers_frame( self.example_response_headers diff --git a/test/test_header_indexing.py b/test/test_header_indexing.py index 23fd06f15..122db3305 100644 --- a/test/test_header_indexing.py +++ b/test/test_header_indexing.py @@ -32,10 +32,10 @@ class TestHeaderIndexing(object): the appropriate hpack data structures. """ example_request_headers = [ - HeaderTuple(u':authority', u'example.com'), - HeaderTuple(u':path', u'/'), - HeaderTuple(u':scheme', u'https'), - HeaderTuple(u':method', u'GET'), + HeaderTuple(':authority', 'example.com'), + HeaderTuple(':path', '/'), + HeaderTuple(':scheme', 'https'), + HeaderTuple(':method', 'GET'), ] bytes_example_request_headers = [ HeaderTuple(b':authority', b'example.com'), @@ -45,11 +45,11 @@ class TestHeaderIndexing(object): ] extended_request_headers = [ - HeaderTuple(u':authority', u'example.com'), - HeaderTuple(u':path', u'/'), - HeaderTuple(u':scheme', u'https'), - HeaderTuple(u':method', u'GET'), - NeverIndexedHeaderTuple(u'authorization', u'realpassword'), + HeaderTuple(':authority', 'example.com'), + HeaderTuple(':path', '/'), + HeaderTuple(':scheme', 'https'), + HeaderTuple(':method', 'GET'), + NeverIndexedHeaderTuple('authorization', 'realpassword'), ] bytes_extended_request_headers = [ HeaderTuple(b':authority', b'example.com'), @@ -60,8 +60,8 @@ class TestHeaderIndexing(object): ] example_response_headers = [ - HeaderTuple(u':status', u'200'), - HeaderTuple(u'server', u'fake-serv/0.1.0') + HeaderTuple(':status', '200'), + HeaderTuple('server', 'fake-serv/0.1.0') ] bytes_example_response_headers = [ HeaderTuple(b':status', b'200'), @@ -69,9 +69,9 @@ class TestHeaderIndexing(object): ] extended_response_headers = [ - HeaderTuple(u':status', u'200'), - HeaderTuple(u'server', u'fake-serv/0.1.0'), - NeverIndexedHeaderTuple(u'secure', u'you-bet'), + HeaderTuple(':status', '200'), + HeaderTuple('server', 'fake-serv/0.1.0'), + NeverIndexedHeaderTuple('secure', 'you-bet'), ] bytes_extended_response_headers = [ HeaderTuple(b':status', b'200'), @@ -228,7 +228,7 @@ def test_header_tuples_are_decoded_info_response(self, # to avoid breaking the example headers. headers = headers[:] if encoding: - headers[0] = HeaderTuple(u':status', u'100') + headers[0] = HeaderTuple(':status', '100') else: headers[0] = HeaderTuple(b':status', b'100') @@ -334,10 +334,10 @@ class TestSecureHeaders(object): Certain headers should always be transformed to their never-indexed form. """ example_request_headers = [ - (u':authority', u'example.com'), - (u':path', u'/'), - (u':scheme', u'https'), - (u':method', u'GET'), + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'GET'), ] bytes_example_request_headers = [ (b':authority', b'example.com'), @@ -346,15 +346,15 @@ class TestSecureHeaders(object): (b':method', b'GET'), ] possible_auth_headers = [ - (u'authorization', u'test'), - (u'Authorization', u'test'), - (u'authorization', u'really long test'), - HeaderTuple(u'authorization', u'test'), - HeaderTuple(u'Authorization', u'test'), - HeaderTuple(u'authorization', u'really long test'), - NeverIndexedHeaderTuple(u'authorization', u'test'), - NeverIndexedHeaderTuple(u'Authorization', u'test'), - NeverIndexedHeaderTuple(u'authorization', u'really long test'), + ('authorization', 'test'), + ('Authorization', 'test'), + ('authorization', 'really long test'), + HeaderTuple('authorization', 'test'), + HeaderTuple('Authorization', 'test'), + HeaderTuple('authorization', 'really long test'), + NeverIndexedHeaderTuple('authorization', 'test'), + NeverIndexedHeaderTuple('Authorization', 'test'), + NeverIndexedHeaderTuple('authorization', 'really long test'), (b'authorization', b'test'), (b'Authorization', b'test'), (b'authorization', b'really long test'), @@ -364,15 +364,15 @@ class TestSecureHeaders(object): NeverIndexedHeaderTuple(b'authorization', b'test'), NeverIndexedHeaderTuple(b'Authorization', b'test'), NeverIndexedHeaderTuple(b'authorization', b'really long test'), - (u'proxy-authorization', u'test'), - (u'Proxy-Authorization', u'test'), - (u'proxy-authorization', u'really long test'), - HeaderTuple(u'proxy-authorization', u'test'), - HeaderTuple(u'Proxy-Authorization', u'test'), - HeaderTuple(u'proxy-authorization', u'really long test'), - NeverIndexedHeaderTuple(u'proxy-authorization', u'test'), - NeverIndexedHeaderTuple(u'Proxy-Authorization', u'test'), - NeverIndexedHeaderTuple(u'proxy-authorization', u'really long test'), + ('proxy-authorization', 'test'), + ('Proxy-Authorization', 'test'), + ('proxy-authorization', 'really long test'), + HeaderTuple('proxy-authorization', 'test'), + HeaderTuple('Proxy-Authorization', 'test'), + HeaderTuple('proxy-authorization', 'really long test'), + NeverIndexedHeaderTuple('proxy-authorization', 'test'), + NeverIndexedHeaderTuple('Proxy-Authorization', 'test'), + NeverIndexedHeaderTuple('proxy-authorization', 'really long test'), (b'proxy-authorization', b'test'), (b'Proxy-Authorization', b'test'), (b'proxy-authorization', b'really long test'), @@ -384,16 +384,16 @@ class TestSecureHeaders(object): NeverIndexedHeaderTuple(b'proxy-authorization', b'really long test'), ] secured_cookie_headers = [ - (u'cookie', u'short'), - (u'Cookie', u'short'), - (u'cookie', u'nineteen byte cooki'), - HeaderTuple(u'cookie', u'short'), - HeaderTuple(u'Cookie', u'short'), - HeaderTuple(u'cookie', u'nineteen byte cooki'), - NeverIndexedHeaderTuple(u'cookie', u'short'), - NeverIndexedHeaderTuple(u'Cookie', u'short'), - NeverIndexedHeaderTuple(u'cookie', u'nineteen byte cooki'), - NeverIndexedHeaderTuple(u'cookie', u'longer manually secured cookie'), + ('cookie', 'short'), + ('Cookie', 'short'), + ('cookie', 'nineteen byte cooki'), + HeaderTuple('cookie', 'short'), + HeaderTuple('Cookie', 'short'), + HeaderTuple('cookie', 'nineteen byte cooki'), + NeverIndexedHeaderTuple('cookie', 'short'), + NeverIndexedHeaderTuple('Cookie', 'short'), + NeverIndexedHeaderTuple('cookie', 'nineteen byte cooki'), + NeverIndexedHeaderTuple('cookie', 'longer manually secured cookie'), (b'cookie', b'short'), (b'Cookie', b'short'), (b'cookie', b'nineteen byte cooki'), @@ -406,12 +406,12 @@ class TestSecureHeaders(object): NeverIndexedHeaderTuple(b'cookie', b'longer manually secured cookie'), ] unsecured_cookie_headers = [ - (u'cookie', u'twenty byte cookie!!'), - (u'Cookie', u'twenty byte cookie!!'), - (u'cookie', u'substantially longer than 20 byte cookie'), - HeaderTuple(u'cookie', u'twenty byte cookie!!'), - HeaderTuple(u'cookie', u'twenty byte cookie!!'), - HeaderTuple(u'Cookie', u'twenty byte cookie!!'), + ('cookie', 'twenty byte cookie!!'), + ('Cookie', 'twenty byte cookie!!'), + ('cookie', 'substantially longer than 20 byte cookie'), + HeaderTuple('cookie', 'twenty byte cookie!!'), + HeaderTuple('cookie', 'twenty byte cookie!!'), + HeaderTuple('Cookie', 'twenty byte cookie!!'), (b'cookie', b'twenty byte cookie!!'), (b'Cookie', b'twenty byte cookie!!'), (b'cookie', b'substantially longer than 20 byte cookie'), diff --git a/test/test_informational_responses.py b/test/test_informational_responses.py index e18c44bcb..64d3a6e74 100644 --- a/test/test_informational_responses.py +++ b/test/test_informational_responses.py @@ -216,15 +216,22 @@ class TestSendingInformationalResponses(object): Tests for sending informational responses. """ example_request_headers = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'GET'), + ('expect', '100-continue'), + ] + bytes_example_request_headers = [ (b':authority', b'example.com'), (b':path', b'/'), (b':scheme', b'https'), (b':method', b'GET'), (b'expect', b'100-continue'), ] - unicode_informational_headers = [ - (u':status', u'100'), - (u'server', u'fake-serv/0.1.0') + informational_headers = [ + (':status', '100'), + ('server', 'fake-serv/0.1.0') ] bytes_informational_headers = [ (b':status', b'100'), @@ -240,12 +247,16 @@ class TestSendingInformationalResponses(object): server_config = h2.config.H2Configuration(client_side=False) @pytest.mark.parametrize( - 'hdrs', (unicode_informational_headers, bytes_informational_headers), + 'hdrs', (informational_headers, bytes_informational_headers), + ) + @pytest.mark.parametrize( + 'request_headers', (example_request_headers, bytes_example_request_headers), ) @pytest.mark.parametrize('end_stream', (True, False)) def test_single_informational_response(self, frame_factory, hdrs, + request_headers, end_stream): """ When sending a informational response, the appropriate frames are @@ -256,7 +267,7 @@ def test_single_informational_response(self, c.receive_data(frame_factory.preamble()) flags = ['END_STREAM'] if end_stream else [] f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=flags, ) @@ -276,12 +287,16 @@ def test_single_informational_response(self, assert c.data_to_send() == f.serialize() @pytest.mark.parametrize( - 'hdrs', (unicode_informational_headers, bytes_informational_headers), + 'hdrs', (informational_headers, bytes_informational_headers), + ) + @pytest.mark.parametrize( + 'request_headers', (example_request_headers, bytes_example_request_headers), ) @pytest.mark.parametrize('end_stream', (True, False)) def test_sending_multiple_header_blocks(self, frame_factory, hdrs, + request_headers, end_stream): """ At least three header blocks can be sent: informational, headers, @@ -292,7 +307,7 @@ def test_sending_multiple_header_blocks(self, c.receive_data(frame_factory.preamble()) flags = ['END_STREAM'] if end_stream else [] f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=flags, ) @@ -335,12 +350,16 @@ def test_sending_multiple_header_blocks(self, ) @pytest.mark.parametrize( - 'hdrs', (unicode_informational_headers, bytes_informational_headers), + 'hdrs', (informational_headers, bytes_informational_headers), + ) + @pytest.mark.parametrize( + 'request_headers', (example_request_headers, bytes_example_request_headers), ) @pytest.mark.parametrize('end_stream', (True, False)) def test_sending_multiple_informational_responses(self, frame_factory, hdrs, + request_headers, end_stream): """ More than one informational response is allowed. @@ -350,7 +369,7 @@ def test_sending_multiple_informational_responses(self, c.receive_data(frame_factory.preamble()) flags = ['END_STREAM'] if end_stream else [] f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=flags, ) @@ -365,7 +384,7 @@ def test_sending_multiple_informational_responses(self, ) c.send_headers( stream_id=1, - headers=[(':status', '101')] + headers=[(b':status', b'101')] ) # Check we sent them both. @@ -380,12 +399,16 @@ def test_sending_multiple_informational_responses(self, assert c.data_to_send() == f1.serialize() + f2.serialize() @pytest.mark.parametrize( - 'hdrs', (unicode_informational_headers, bytes_informational_headers), + 'hdrs', (informational_headers, bytes_informational_headers), + ) + @pytest.mark.parametrize( + 'request_headers', (example_request_headers, bytes_example_request_headers), ) @pytest.mark.parametrize('end_stream', (True, False)) def test_send_provisional_response_with_end_stream(self, frame_factory, hdrs, + request_headers, end_stream): """ Sending provisional responses with END_STREAM set causes @@ -396,7 +419,7 @@ def test_send_provisional_response_with_end_stream(self, c.receive_data(frame_factory.preamble()) flags = ['END_STREAM'] if end_stream else [] f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=flags, ) @@ -410,12 +433,16 @@ def test_send_provisional_response_with_end_stream(self, ) @pytest.mark.parametrize( - 'hdrs', (unicode_informational_headers, bytes_informational_headers), + 'hdrs', (informational_headers, bytes_informational_headers), + ) + @pytest.mark.parametrize( + 'request_headers', (example_request_headers, bytes_example_request_headers), ) @pytest.mark.parametrize('end_stream', (True, False)) def test_reject_sending_out_of_order_headers(self, frame_factory, hdrs, + request_headers, end_stream): """ When sending an informational response after the actual response @@ -426,7 +453,7 @@ def test_reject_sending_out_of_order_headers(self, c.receive_data(frame_factory.preamble()) flags = ['END_STREAM'] if end_stream else [] f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=flags, ) diff --git a/test/test_interacting_stacks.py b/test/test_interacting_stacks.py index 32c967324..70f2235b6 100644 --- a/test/test_interacting_stacks.py +++ b/test/test_interacting_stacks.py @@ -20,6 +20,8 @@ """ from . import coroutine_tests +import pytest + import h2.config import h2.connection import h2.events @@ -32,23 +34,33 @@ class TestCommunication(coroutine_tests.CoroutineTestCase): """ server_config = h2.config.H2Configuration(client_side=False) - def test_basic_request_response(self): + request_headers = [ + (':method', 'GET'), + (':path', '/'), + (':authority', 'example.com'), + (':scheme', 'https'), + ('user-agent', 'test-client/0.1.0'), + ] + + request_headers_bytes = [ + (b':method', b'GET'), + (b':path', b'/'), + (b':authority', b'example.com'), + (b':scheme', b'https'), + (b'user-agent', b'test-client/0.1.0'), + ] + + response_headers = [ + (b':status', b'204'), + (b'server', b'test-server/0.1.0'), + (b'content-length', b'0'), + ] + + @pytest.mark.parametrize('request_headers', [request_headers, request_headers_bytes]) + def test_basic_request_response(self, request_headers): """ A request issued by hyper-h2 can be responded to by hyper-h2. """ - request_headers = [ - (b':method', b'GET'), - (b':path', b'/'), - (b':authority', b'example.com'), - (b':scheme', b'https'), - (b'user-agent', b'test-client/0.1.0'), - ] - response_headers = [ - (b':status', b'204'), - (b'server', b'test-server/0.1.0'), - (b'content-length', b'0'), - ] - def client(): c = h2.connection.H2Connection() @@ -78,7 +90,7 @@ def client(): assert len(events) == 2 assert isinstance(events[0], h2.events.ResponseReceived) assert events[0].stream_id == 1 - assert events[0].headers == response_headers + assert events[0].headers == self.response_headers assert isinstance(events[1], h2.events.StreamEnded) assert events[1].stream_id == 1 @@ -108,12 +120,12 @@ def server(): assert isinstance(events[0], h2.events.SettingsAcknowledged) assert isinstance(events[1], h2.events.RequestReceived) assert events[1].stream_id == 1 - assert events[1].headers == request_headers + assert events[1].headers == self.request_headers_bytes assert isinstance(events[2], h2.events.StreamEnded) assert events[2].stream_id == 1 # Send our response. - events = c.send_headers(1, response_headers, end_stream=True) + events = c.send_headers(1, self.response_headers, end_stream=True) assert not events yield c.data_to_send() diff --git a/test/test_invalid_content_lengths.py b/test/test_invalid_content_lengths.py index fe682fcc2..b33e9c6ad 100644 --- a/test/test_invalid_content_lengths.py +++ b/test/test_invalid_content_lengths.py @@ -27,13 +27,21 @@ class TestInvalidContentLengths(object): (':method', 'POST'), ('content-length', '15'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'POST'), + (b'content-length', b'15'), + ] example_response_headers = [ (':status', '200'), ('server', 'fake-serv/0.1.0') ] server_config = h2.config.H2Configuration(client_side=False) - def test_too_much_data(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_too_much_data(self, frame_factory, request_headers): """ Remote peers sending data in excess of content-length causes Protocol Errors. @@ -43,7 +51,7 @@ def test_too_much_data(self, frame_factory): c.receive_data(frame_factory.preamble()) headers = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) first_data = frame_factory.build_data_frame(data=b'\x01'*15) c.receive_data(headers.serialize() + first_data.serialize()) @@ -65,7 +73,8 @@ def test_too_much_data(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_insufficient_data(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_insufficient_data(self, frame_factory, request_headers): """ Remote peers sending less data than content-length causes Protocol Errors. @@ -75,7 +84,7 @@ def test_insufficient_data(self, frame_factory): c.receive_data(frame_factory.preamble()) headers = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) first_data = frame_factory.build_data_frame(data=b'\x01'*13) c.receive_data(headers.serialize() + first_data.serialize()) @@ -100,7 +109,8 @@ def test_insufficient_data(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_insufficient_data_empty_frame(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_insufficient_data_empty_frame(self, frame_factory, request_headers): """ Remote peers sending less data than content-length where the last data frame is empty causes Protocol Errors. @@ -110,7 +120,7 @@ def test_insufficient_data_empty_frame(self, frame_factory): c.receive_data(frame_factory.preamble()) headers = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) first_data = frame_factory.build_data_frame(data=b'\x01'*14) c.receive_data(headers.serialize() + first_data.serialize()) diff --git a/test/test_invalid_frame_sequences.py b/test/test_invalid_frame_sequences.py index 05832cbb4..81bf07c4d 100644 --- a/test/test_invalid_frame_sequences.py +++ b/test/test_invalid_frame_sequences.py @@ -26,6 +26,12 @@ class TestInvalidFrameSequences(object): (':scheme', 'https'), (':method', 'GET'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), + ] example_response_headers = [ (':status', '200'), ('server', 'fake-serv/0.1.0') @@ -33,13 +39,14 @@ class TestInvalidFrameSequences(object): server_config = h2.config.H2Configuration(client_side=False) client_config = h2.config.H2Configuration(client_side=True) - def test_cannot_send_on_closed_stream(self): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_send_on_closed_stream(self, request_headers): """ When we've closed a stream locally, we cannot send further data. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(1, self.example_request_headers, end_stream=True) + c.send_headers(1, request_headers, end_stream=True) with pytest.raises(h2.exceptions.ProtocolError): c.send_data(1, b'some data') @@ -57,7 +64,8 @@ def test_missing_preamble_errors(self): with pytest.raises(h2.exceptions.ProtocolError): c.receive_data(encoded_headers_frame) - def test_server_connections_reject_even_streams(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_server_connections_reject_even_streams(self, frame_factory, request_headers): """ Servers do not allow clients to initiate even-numbered streams. """ @@ -66,30 +74,32 @@ def test_server_connections_reject_even_streams(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, stream_id=2 + request_headers, stream_id=2 ) with pytest.raises(h2.exceptions.ProtocolError): c.receive_data(f.serialize()) - def test_clients_reject_odd_stream_pushes(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_clients_reject_odd_stream_pushes(self, frame_factory, request_headers): """ Clients do not allow servers to push odd numbered streams. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(1, self.example_request_headers, end_stream=True) + c.send_headers(1, request_headers, end_stream=True) f = frame_factory.build_push_promise_frame( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, promised_stream_id=3 ) with pytest.raises(h2.exceptions.ProtocolError): c.receive_data(f.serialize()) - def test_can_handle_frames_with_invalid_padding(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_can_handle_frames_with_invalid_padding(self, frame_factory, request_headers): """ Frames with invalid padding cause connection teardown. """ @@ -97,7 +107,7 @@ def test_can_handle_frames_with_invalid_padding(self, frame_factory): c.initiate_connection() c.receive_data(frame_factory.preamble()) - f = frame_factory.build_headers_frame(self.example_request_headers) + f = frame_factory.build_headers_frame(request_headers) c.receive_data(f.serialize()) c.clear_outbound_data_buffer() @@ -134,7 +144,8 @@ def test_receiving_frames_with_insufficent_size(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_reject_data_on_closed_streams(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_reject_data_on_closed_streams(self, frame_factory, request_headers): """ When a stream is not open to the remote peer, we reject receiving data frames from them. @@ -144,7 +155,7 @@ def test_reject_data_on_closed_streams(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, + request_headers, flags=['END_STREAM'] ) c.receive_data(f.serialize()) @@ -161,7 +172,8 @@ def test_reject_data_on_closed_streams(self, frame_factory): ).serialize() assert c.data_to_send() == expected - def test_unexpected_continuation_on_closed_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_unexpected_continuation_on_closed_stream(self, frame_factory, request_headers): """ CONTINUATION frames received on closed streams cause connection errors of type PROTOCOL_ERROR. @@ -171,7 +183,7 @@ def test_unexpected_continuation_on_closed_stream(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, + request_headers, flags=['END_STREAM'] ) c.receive_data(f.serialize()) @@ -190,7 +202,8 @@ def test_unexpected_continuation_on_closed_stream(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_prevent_continuation_dos(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_prevent_continuation_dos(self, frame_factory, request_headers): """ Receiving too many CONTINUATION frames in one block causes a protocol error. @@ -200,7 +213,7 @@ def test_prevent_continuation_dos(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, + request_headers, ) f.flags = {'END_STREAM'} c.receive_data(f.serialize()) @@ -255,7 +268,8 @@ def test_reject_invalid_settings_values(self, frame_factory, settings): h2.errors.ErrorCodes.PROTOCOL_ERROR ) - def test_invalid_frame_headers_are_protocol_errors(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_invalid_frame_headers_are_protocol_errors(self, frame_factory, request_headers): """ When invalid frame headers are received they cause ProtocolErrors to be raised. @@ -265,7 +279,7 @@ def test_invalid_frame_headers_are_protocol_errors(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) # Do some annoying bit twiddling here: the stream ID is currently set @@ -280,7 +294,8 @@ def test_invalid_frame_headers_are_protocol_errors(self, frame_factory): assert "Received frame with invalid header" in str(e.value) - def test_data_before_headers(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_data_before_headers(self, frame_factory, request_headers): """ When data frames are received before headers they cause ProtocolErrors to be raised. @@ -288,7 +303,7 @@ def test_data_before_headers(self, frame_factory): c = h2.connection.H2Connection(config=self.client_config) c.initiate_connection() # transition stream into OPEN - c.send_headers(1, self.example_request_headers) + c.send_headers(1, request_headers) f = frame_factory.build_data_frame(b"hello") @@ -297,7 +312,8 @@ def test_data_before_headers(self, frame_factory): assert "cannot receive data before headers" in str(e.value) - def test_get_stream_reset_event_on_auto_reset(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_get_stream_reset_event_on_auto_reset(self, frame_factory, request_headers): """ When hyper-h2 resets a stream automatically, a StreamReset event fires. """ @@ -306,7 +322,7 @@ def test_get_stream_reset_event_on_auto_reset(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, + request_headers, flags=['END_STREAM'] ) c.receive_data(f.serialize()) @@ -330,7 +346,8 @@ def test_get_stream_reset_event_on_auto_reset(self, frame_factory): assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED assert not event.remote_reset - def test_one_one_stream_reset(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_one_one_stream_reset(self, frame_factory, request_headers): """ When hyper-h2 resets a stream automatically, a StreamReset event fires, but only for the first reset: the others are silent. @@ -340,7 +357,7 @@ def test_one_one_stream_reset(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - self.example_request_headers, + request_headers, flags=['END_STREAM'] ) c.receive_data(f.serialize()) @@ -365,8 +382,9 @@ def test_one_one_stream_reset(self, frame_factory): assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED assert not event.remote_reset + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) @pytest.mark.parametrize('value', ['', 'twelve']) - def test_error_on_invalid_content_length(self, frame_factory, value): + def test_error_on_invalid_content_length(self, frame_factory, value, request_headers): """ When an invalid content-length is received, a ProtocolError is thrown. """ @@ -377,7 +395,7 @@ def test_error_on_invalid_content_length(self, frame_factory, value): f = frame_factory.build_headers_frame( stream_id=1, - headers=self.example_request_headers + [('content-length', value)] + headers=request_headers + [('content-length', value)] ) with pytest.raises(h2.exceptions.ProtocolError): c.receive_data(f.serialize()) @@ -388,7 +406,8 @@ def test_error_on_invalid_content_length(self, frame_factory, value): ) assert c.data_to_send() == expected_frame.serialize() - def test_invalid_header_data_protocol_error(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_invalid_header_data_protocol_error(self, frame_factory, request_headers): """ If an invalid header block is received, we raise a ProtocolError. """ @@ -399,7 +418,7 @@ def test_invalid_header_data_protocol_error(self, frame_factory): f = frame_factory.build_headers_frame( stream_id=1, - headers=self.example_request_headers + headers=request_headers ) f.data = b'\x00\x00\x00\x00' @@ -412,20 +431,21 @@ def test_invalid_header_data_protocol_error(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_invalid_push_promise_data_protocol_error(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_invalid_push_promise_data_protocol_error(self, frame_factory, request_headers): """ If an invalid header block is received on a PUSH_PROMISE, we raise a ProtocolError. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) c.clear_outbound_data_buffer() f = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, - headers=self.example_request_headers + headers=request_headers ) f.data = b'\x00\x00\x00\x00' @@ -438,7 +458,8 @@ def test_invalid_push_promise_data_protocol_error(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_cannot_receive_push_on_pushed_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_receive_push_on_pushed_stream(self, frame_factory, request_headers): """ If a PUSH_PROMISE frame is received with the parent stream ID being a pushed stream, this is rejected with a PROTOCOL_ERROR. @@ -447,14 +468,14 @@ def test_cannot_receive_push_on_pushed_stream(self, frame_factory): c.initiate_connection() c.send_headers( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, end_stream=True ) f1 = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, - headers=self.example_request_headers, + headers=request_headers, ) f2 = frame_factory.build_headers_frame( stream_id=2, @@ -466,7 +487,7 @@ def test_cannot_receive_push_on_pushed_stream(self, frame_factory): f = frame_factory.build_push_promise_frame( stream_id=2, promised_stream_id=4, - headers=self.example_request_headers, + headers=request_headers, ) with pytest.raises(h2.exceptions.ProtocolError): @@ -478,7 +499,8 @@ def test_cannot_receive_push_on_pushed_stream(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() - def test_cannot_send_push_on_pushed_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_send_push_on_pushed_stream(self, frame_factory, request_headers): """ If a user tries to send a PUSH_PROMISE frame with the parent stream ID being a pushed stream, this is rejected with a PROTOCOL_ERROR. @@ -487,14 +509,14 @@ def test_cannot_send_push_on_pushed_stream(self, frame_factory): c.initiate_connection() c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - stream_id=1, headers=self.example_request_headers + stream_id=1, headers=request_headers ) c.receive_data(f.serialize()) c.push_stream( stream_id=1, promised_stream_id=2, - request_headers=self.example_request_headers + request_headers=request_headers ) c.send_headers(stream_id=2, headers=self.example_response_headers) @@ -502,5 +524,5 @@ def test_cannot_send_push_on_pushed_stream(self, frame_factory): c.push_stream( stream_id=2, promised_stream_id=4, - request_headers=self.example_request_headers + request_headers=request_headers ) diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 2690d3140..996e48aa6 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -37,7 +37,7 @@ class TestInvalidFrameSequences(object): (':method', 'GET'), ('user-agent', 'someua/0.0.1'), ] - invalid_header_blocks = [ + base_invalid_header_blocks = [ base_request_headers + [('Uppercase', 'name')], base_request_headers + [(':late', 'pseudo-header')], [(':path', 'duplicate-pseudo-header')] + base_request_headers, @@ -56,6 +56,9 @@ class TestInvalidFrameSequences(object): if header[0] != ':authority'], [(':protocol', 'websocket')] + base_request_headers, ] + invalid_header_blocks = base_invalid_header_blocks + [ + h2.utilities.utf8_encode_headers(headers) for headers in base_invalid_header_blocks + ] server_config = h2.config.H2Configuration( client_side=False, header_encoding='utf-8' ) @@ -117,7 +120,6 @@ def test_push_promise_skipping_validation(self, frame_factory, headers): config = h2.config.H2Configuration( client_side=True, validate_inbound_headers=False, - header_encoding='utf-8' ) c = h2.connection.H2Connection(config=config) @@ -137,7 +139,7 @@ def test_push_promise_skipping_validation(self, frame_factory, headers): events = c.receive_data(data) assert len(events) == 1 pp_event = events[0] - assert pp_event.headers == headers + assert pp_event.headers == h2.utilities.utf8_encode_headers(headers) @pytest.mark.parametrize('headers', invalid_header_blocks) def test_headers_event_skipping_validation(self, frame_factory, headers): @@ -148,7 +150,6 @@ def test_headers_event_skipping_validation(self, frame_factory, headers): config = h2.config.H2Configuration( client_side=False, validate_inbound_headers=False, - header_encoding='utf-8' ) c = h2.connection.H2Connection(config=config) @@ -160,7 +161,7 @@ def test_headers_event_skipping_validation(self, frame_factory, headers): events = c.receive_data(data) assert len(events) == 1 request_event = events[0] - assert request_event.headers == headers + assert request_event.headers == h2.utilities.utf8_encode_headers(headers) def test_te_trailers_is_valid(self, frame_factory): """ diff --git a/test/test_priority.py b/test/test_priority.py index cbc733225..086cf68f6 100644 --- a/test/test_priority.py +++ b/test/test_priority.py @@ -25,6 +25,12 @@ class TestPriority(object): (':scheme', 'https'), (':method', 'GET'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), + ] example_response_headers = [ (':status', '200'), ('server', 'pytest-h2'), @@ -56,7 +62,8 @@ def test_receiving_priority_emits_priority_update(self, frame_factory): assert event.weight == 256 assert event.exclusive is False - def test_headers_with_priority_info(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_headers_with_priority_info(self, frame_factory, request_headers): """ Receiving a HEADERS frame with priority information on it emits a PriorityUpdated event. @@ -67,7 +74,7 @@ def test_headers_with_priority_info(self, frame_factory): c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, flags=['PRIORITY'], stream_weight=15, @@ -86,7 +93,8 @@ def test_headers_with_priority_info(self, frame_factory): assert event.weight == 16 assert event.exclusive is True - def test_streams_may_not_depend_on_themselves(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_streams_may_not_depend_on_themselves(self, frame_factory, request_headers): """ A stream adjusted to depend on itself causes a Protocol Error. """ @@ -96,7 +104,7 @@ def test_streams_may_not_depend_on_themselves(self, frame_factory): c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, flags=['PRIORITY'], stream_weight=15, @@ -120,6 +128,7 @@ def test_streams_may_not_depend_on_themselves(self, frame_factory): ) assert c.data_to_send() == expected_frame.serialize() + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) @pytest.mark.parametrize( 'depends_on,weight,exclusive', [ @@ -129,15 +138,15 @@ def test_streams_may_not_depend_on_themselves(self, frame_factory): ] ) def test_can_prioritize_stream(self, depends_on, weight, exclusive, - frame_factory): + frame_factory, request_headers): """ hyper-h2 can emit priority frames. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(headers=self.example_request_headers, stream_id=1) - c.send_headers(headers=self.example_request_headers, stream_id=3) + c.send_headers(headers=request_headers, stream_id=1) + c.send_headers(headers=request_headers, stream_id=3) c.clear_outbound_data_buffer() c.prioritize( @@ -155,6 +164,7 @@ def test_can_prioritize_stream(self, depends_on, weight, exclusive, ) assert c.data_to_send() == f.serialize() + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) @pytest.mark.parametrize( 'depends_on,weight,exclusive', [ @@ -164,7 +174,7 @@ def test_can_prioritize_stream(self, depends_on, weight, exclusive, ] ) def test_emit_headers_with_priority_info(self, depends_on, weight, - exclusive, frame_factory): + exclusive, frame_factory, request_headers): """ It is possible to send a headers frame with priority information on it. @@ -174,7 +184,7 @@ def test_emit_headers_with_priority_info(self, depends_on, weight, c.clear_outbound_data_buffer() c.send_headers( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, priority_weight=weight, priority_depends_on=depends_on, @@ -182,7 +192,7 @@ def test_emit_headers_with_priority_info(self, depends_on, weight, ) f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, flags=['PRIORITY'], stream_weight=weight - 1, @@ -191,7 +201,8 @@ def test_emit_headers_with_priority_info(self, depends_on, weight, ) assert c.data_to_send() == f.serialize() - def test_may_not_prioritize_stream_to_depend_on_self(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_may_not_prioritize_stream_to_depend_on_self(self, frame_factory, request_headers): """ A stream adjusted to depend on itself causes a Protocol Error. """ @@ -199,7 +210,7 @@ def test_may_not_prioritize_stream_to_depend_on_self(self, frame_factory): c.initiate_connection() c.receive_data(frame_factory.preamble()) c.send_headers( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, priority_weight=255, priority_depends_on=0, @@ -215,7 +226,8 @@ def test_may_not_prioritize_stream_to_depend_on_self(self, frame_factory): assert not c.data_to_send() - def test_may_not_initially_set_stream_depend_on_self(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_may_not_initially_set_stream_depend_on_self(self, frame_factory, request_headers): """ A stream that starts by depending on itself causes a Protocol Error. """ @@ -226,7 +238,7 @@ def test_may_not_initially_set_stream_depend_on_self(self, frame_factory): with pytest.raises(h2.exceptions.ProtocolError): c.send_headers( - headers=self.example_request_headers, + headers=request_headers, stream_id=3, priority_depends_on=3, ) @@ -247,8 +259,9 @@ def test_prioritize_requires_valid_weight(self, weight): assert not c.data_to_send() + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) @pytest.mark.parametrize('weight', [0, -15, 257]) - def test_send_headers_requires_valid_weight(self, weight): + def test_send_headers_requires_valid_weight(self, weight, request_headers): """ A call to send_headers with an invalid weight causes a ProtocolError. """ @@ -259,7 +272,7 @@ def test_send_headers_requires_valid_weight(self, weight): with pytest.raises(h2.exceptions.ProtocolError): c.send_headers( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, priority_weight=weight ) @@ -284,6 +297,7 @@ def test_prioritize_defaults(self, frame_factory): ) assert c.data_to_send() == f.serialize() + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) @pytest.mark.parametrize( 'priority_kwargs', [ @@ -292,7 +306,7 @@ def test_prioritize_defaults(self, frame_factory): {'priority_exclusive': False}, ] ) - def test_send_headers_defaults(self, priority_kwargs, frame_factory): + def test_send_headers_defaults(self, priority_kwargs, frame_factory, request_headers): """ When send_headers() is called with only one explicit argument, it emits default values for everything else. @@ -303,12 +317,12 @@ def test_send_headers_defaults(self, priority_kwargs, frame_factory): c.send_headers( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, **priority_kwargs ) f = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, stream_id=1, flags=['PRIORITY'], stream_weight=15, @@ -317,7 +331,8 @@ def test_send_headers_defaults(self, priority_kwargs, frame_factory): ) assert c.data_to_send() == f.serialize() - def test_servers_cannot_prioritize(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_servers_cannot_prioritize(self, frame_factory, request_headers): """ Server stacks are not allowed to call ``prioritize()``. """ @@ -328,14 +343,15 @@ def test_servers_cannot_prioritize(self, frame_factory): f = frame_factory.build_headers_frame( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, ) c.receive_data(f.serialize()) with pytest.raises(h2.exceptions.RFC1122Error): c.prioritize(stream_id=1) - def test_servers_cannot_prioritize_with_headers(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_servers_cannot_prioritize_with_headers(self, frame_factory, request_headers): """ Server stacks are not allowed to prioritize on headers either. """ @@ -346,7 +362,7 @@ def test_servers_cannot_prioritize_with_headers(self, frame_factory): f = frame_factory.build_headers_frame( stream_id=1, - headers=self.example_request_headers, + headers=request_headers, ) c.receive_data(f.serialize()) diff --git a/test/test_related_events.py b/test/test_related_events.py index eb6b87890..193344084 100644 --- a/test/test_related_events.py +++ b/test/test_related_events.py @@ -6,6 +6,8 @@ Specific tests to validate the "related events" logic used by certain events inside hyper-h2. """ +import pytest + import h2.config import h2.connection import h2.events @@ -22,6 +24,13 @@ class TestRelatedEvents(object): (':method', 'GET'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), + ] + example_response_headers = [ (':status', '200'), ('server', 'fake-serv/0.1.0') @@ -38,7 +47,8 @@ class TestRelatedEvents(object): server_config = h2.config.H2Configuration(client_side=False) - def test_request_received_related_all(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_request_received_related_all(self, frame_factory, request_headers): """ RequestReceived has two possible related events: PriorityUpdated and StreamEnded, all fired when a single HEADERS frame is received. @@ -48,7 +58,7 @@ def test_request_received_related_all(self, frame_factory): c.receive_data(frame_factory.preamble()) input_frame = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, flags=['END_STREAM', 'PRIORITY'], stream_weight=15, depends_on=0, @@ -67,7 +77,8 @@ def test_request_received_related_all(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_request_received_related_priority(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_request_received_related_priority(self, frame_factory, request_headers): """ RequestReceived can be related to PriorityUpdated. """ @@ -76,7 +87,7 @@ def test_request_received_related_priority(self, frame_factory): c.receive_data(frame_factory.preamble()) input_frame = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, flags=['PRIORITY'], stream_weight=15, depends_on=0, @@ -94,7 +105,8 @@ def test_request_received_related_priority(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_request_received_related_stream_ended(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_request_received_related_stream_ended(self, frame_factory, request_headers): """ RequestReceived can be related to StreamEnded. """ @@ -103,7 +115,7 @@ def test_request_received_related_stream_ended(self, frame_factory): c.receive_data(frame_factory.preamble()) input_frame = frame_factory.build_headers_frame( - headers=self.example_request_headers, + headers=request_headers, flags=['END_STREAM'], ) events = c.receive_data(input_frame.serialize()) @@ -116,13 +128,14 @@ def test_request_received_related_stream_ended(self, frame_factory): assert base_event.priority_updated is None assert isinstance(base_event.stream_ended, h2.events.StreamEnded) - def test_response_received_related_nothing(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_response_received_related_nothing(self, frame_factory, request_headers): """ ResponseReceived is ordinarily related to no events. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -135,14 +148,15 @@ def test_response_received_related_nothing(self, frame_factory): assert base_event.stream_ended is None assert base_event.priority_updated is None - def test_response_received_related_all(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_response_received_related_all(self, frame_factory, request_headers): """ ResponseReceived has two possible related events: PriorityUpdated and StreamEnded, all fired when a single HEADERS frame is received. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -164,13 +178,14 @@ def test_response_received_related_all(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_response_received_related_priority(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_response_received_related_priority(self, frame_factory, request_headers): """ ResponseReceived can be related to PriorityUpdated. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -191,13 +206,14 @@ def test_response_received_related_priority(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_response_received_related_stream_ended(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_response_received_related_stream_ended(self, frame_factory, request_headers): """ ResponseReceived can be related to StreamEnded. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -213,14 +229,15 @@ def test_response_received_related_stream_ended(self, frame_factory): assert base_event.priority_updated is None assert isinstance(base_event.stream_ended, h2.events.StreamEnded) - def test_trailers_received_related_all(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_trailers_received_related_all(self, frame_factory, request_headers): """ TrailersReceived has two possible related events: PriorityUpdated and StreamEnded, all fired when a single HEADERS frame is received. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -247,13 +264,14 @@ def test_trailers_received_related_all(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_trailers_received_related_stream_ended(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_trailers_received_related_stream_ended(self, frame_factory, request_headers): """ TrailersReceived can be related to StreamEnded by itself. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -274,14 +292,15 @@ def test_trailers_received_related_stream_ended(self, frame_factory): assert base_event.priority_updated is None assert isinstance(base_event.stream_ended, h2.events.StreamEnded) - def test_informational_response_related_nothing(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_informational_response_related_nothing(self, frame_factory, request_headers): """ InformationalResponseReceived in the standard case is related to nothing. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.informational_response_headers, @@ -293,14 +312,15 @@ def test_informational_response_related_nothing(self, frame_factory): assert base_event.priority_updated is None - def test_informational_response_received_related_all(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_informational_response_received_related_all(self, frame_factory, request_headers): """ InformationalResponseReceived has one possible related event: PriorityUpdated, fired when a single HEADERS frame is received. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) input_frame = frame_factory.build_headers_frame( headers=self.informational_response_headers, @@ -320,13 +340,14 @@ def test_informational_response_received_related_all(self, frame_factory): base_event.priority_updated, h2.events.PriorityUpdated ) - def test_data_received_normally_relates_to_nothing(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_data_received_normally_relates_to_nothing(self, frame_factory, request_headers): """ A plain DATA frame leads to DataReceieved with no related events. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_headers_frame( headers=self.example_response_headers, @@ -343,13 +364,14 @@ def test_data_received_normally_relates_to_nothing(self, frame_factory): assert base_event.stream_ended is None - def test_data_received_related_stream_ended(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_data_received_related_stream_ended(self, frame_factory, request_headers): """ DataReceived can be related to StreamEnded by itself. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_headers_frame( headers=self.example_response_headers, diff --git a/test/test_rfc7838.py b/test/test_rfc7838.py index d7704e234..2396a3f33 100644 --- a/test/test_rfc7838.py +++ b/test/test_rfc7838.py @@ -23,9 +23,15 @@ class TestRFC7838Client(object): (':scheme', 'https'), (':method', 'GET'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), + ] example_response_headers = [ - (u':status', u'200'), - (u'server', u'fake-serv/0.1.0') + (':status', '200'), + ('server', 'fake-serv/0.1.0') ] def test_receiving_altsvc_stream_zero(self, frame_factory): @@ -69,14 +75,15 @@ def test_receiving_altsvc_stream_zero_no_origin(self, frame_factory): assert not events assert not c.data_to_send() - def test_receiving_altsvc_on_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_on_stream(self, frame_factory, request_headers): """ An ALTSVC frame received on a stream correctly transposes all the fields from the frame and attaches the expected origin. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) c.clear_outbound_data_buffer() f = frame_factory.build_alt_svc_frame( @@ -94,14 +101,15 @@ def test_receiving_altsvc_on_stream(self, frame_factory): # No data gets sent. assert not c.data_to_send() - def test_receiving_altsvc_on_stream_with_origin(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_on_stream_with_origin(self, frame_factory, request_headers): """ An ALTSVC frame received on a stream with an origin field present gets ignored. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) c.clear_outbound_data_buffer() f = frame_factory.build_alt_svc_frame( @@ -159,14 +167,15 @@ def test_receiving_altsvc_before_sending_headers(self, frame_factory): assert len(events) == 0 assert not c.data_to_send() - def test_receiving_altsvc_after_receiving_headers(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_after_receiving_headers(self, frame_factory, request_headers): """ When an ALTSVC frame is received but the server has already sent headers it gets ignored. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_headers_frame( headers=self.example_response_headers @@ -182,14 +191,15 @@ def test_receiving_altsvc_after_receiving_headers(self, frame_factory): assert len(events) == 0 assert not c.data_to_send() - def test_receiving_altsvc_on_closed_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_on_closed_stream(self, frame_factory, request_headers): """ When an ALTSVC frame is received on a closed stream, we ignore it. """ c = h2.connection.H2Connection() c.initiate_connection() c.send_headers( - stream_id=1, headers=self.example_request_headers, end_stream=True + stream_id=1, headers=request_headers, end_stream=True ) f = frame_factory.build_headers_frame( @@ -207,19 +217,20 @@ def test_receiving_altsvc_on_closed_stream(self, frame_factory): assert len(events) == 0 assert not c.data_to_send() - def test_receiving_altsvc_on_pushed_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_on_pushed_stream(self, frame_factory, request_headers): """ When an ALTSVC frame is received on a stream that the server pushed, the frame is accepted. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) f = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, - headers=self.example_request_headers + headers=request_headers ) c.receive_data(f.serialize()) c.clear_outbound_data_buffer() @@ -239,13 +250,14 @@ def test_receiving_altsvc_on_pushed_stream(self, frame_factory): # No data gets sent. assert not c.data_to_send() - def test_cannot_send_explicit_alternative_service(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_send_explicit_alternative_service(self, frame_factory, request_headers): """ A client cannot send an explicit alternative service. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) c.clear_outbound_data_buffer() with pytest.raises(h2.exceptions.ProtocolError): @@ -254,13 +266,14 @@ def test_cannot_send_explicit_alternative_service(self, frame_factory): origin=b"example.com", ) - def test_cannot_send_implicit_alternative_service(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_send_implicit_alternative_service(self, frame_factory, request_headers): """ A client cannot send an implicit alternative service. """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers(stream_id=1, headers=self.example_request_headers) + c.send_headers(stream_id=1, headers=request_headers) c.clear_outbound_data_buffer() with pytest.raises(h2.exceptions.ProtocolError): @@ -280,6 +293,12 @@ class TestRFC7838Server(object): (':scheme', 'https'), (':method', 'GET'), ] + example_request_headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'GET'), + ] example_response_headers = [ (u':status', u'200'), (u'server', u'fake-serv/0.1.0') @@ -305,7 +324,8 @@ def test_receiving_altsvc_as_server_stream_zero(self, frame_factory): assert len(events) == 0 assert not c.data_to_send() - def test_receiving_altsvc_as_server_on_stream(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_receiving_altsvc_as_server_on_stream(self, frame_factory, request_headers): """ When an ALTSVC frame is received on a stream and we are a server, we ignore it. @@ -315,7 +335,7 @@ def test_receiving_altsvc_as_server_on_stream(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) c.receive_data(f.serialize()) c.clear_outbound_data_buffer() @@ -347,7 +367,8 @@ def test_sending_explicit_alternative_service(self, frame_factory): ) assert c.data_to_send() == f.serialize() - def test_sending_implicit_alternative_service(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_sending_implicit_alternative_service(self, frame_factory, request_headers): """ A server can send an implicit alternative service. """ @@ -356,7 +377,7 @@ def test_sending_implicit_alternative_service(self, frame_factory): c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) c.receive_data(f.serialize()) c.clear_outbound_data_buffer() @@ -388,8 +409,10 @@ def test_no_implicit_alternative_service_before_headers(self, stream_id=1, ) + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) def test_no_implicit_alternative_service_after_response(self, - frame_factory): + frame_factory, + request_headers): """ If the server has sent response headers, hyper-h2 forbids sending an implicit alternative service. @@ -399,7 +422,7 @@ def test_no_implicit_alternative_service_after_response(self, c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) c.receive_data(f.serialize()) c.send_headers(stream_id=1, headers=self.example_response_headers) @@ -411,7 +434,8 @@ def test_no_implicit_alternative_service_after_response(self, stream_id=1, ) - def test_cannot_provide_origin_and_stream_id(self, frame_factory): + @pytest.mark.parametrize("request_headers", [example_request_headers, example_request_headers_bytes]) + def test_cannot_provide_origin_and_stream_id(self, frame_factory, request_headers): """ The user cannot provide both the origin and stream_id arguments when advertising alternative services. @@ -420,7 +444,7 @@ def test_cannot_provide_origin_and_stream_id(self, frame_factory): c.initiate_connection() c.receive_data(frame_factory.preamble()) f = frame_factory.build_headers_frame( - headers=self.example_request_headers + headers=request_headers ) c.receive_data(f.serialize()) diff --git a/test/test_rfc8441.py b/test/test_rfc8441.py index b2bf881fd..d3bbde40e 100644 --- a/test/test_rfc8441.py +++ b/test/test_rfc8441.py @@ -5,6 +5,9 @@ Test the RFC 8441 extended connect request support. """ +from h2.utilities import utf8_encode_headers +import pytest + import h2.config import h2.connection import h2.events @@ -16,16 +19,26 @@ class TestRFC8441(object): and the server supports receiving it. """ - def test_can_send_headers(self, frame_factory): - headers = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'CONNECT'), - (b':protocol', b'websocket'), - (b'user-agent', b'someua/0.0.1'), - ] + headers = [ + (':authority', 'example.com'), + (':path', '/'), + (':scheme', 'https'), + (':method', 'CONNECT'), + (':protocol', 'websocket'), + ('user-agent', 'someua/0.0.1'), + ] + + headers_bytes = [ + (b':authority', b'example.com'), + (b':path', b'/'), + (b':scheme', b'https'), + (b':method', b'CONNECT'), + (b':protocol', b'websocket'), + (b'user-agent', b'someua/0.0.1'), + ] + @pytest.mark.parametrize("headers", [headers, headers_bytes]) + def test_can_send_headers(self, frame_factory, headers): client = h2.connection.H2Connection() client.initiate_connection() client.send_headers(stream_id=1, headers=headers) @@ -37,4 +50,4 @@ def test_can_send_headers(self, frame_factory): event = events[1] assert isinstance(event, h2.events.RequestReceived) assert event.stream_id == 1 - assert event.headers == headers + assert event.headers == utf8_encode_headers(headers) From ecdbb43e90f007548a3f11be740022862a21c291 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 19 Nov 2024 21:23:50 +0000 Subject: [PATCH 8/8] revert unrelated change --- src/h2/events.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/h2/events.py b/src/h2/events.py index 20cbccf75..66c3cff4a 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -589,9 +589,11 @@ def __init__(self): self.field_value = None def __repr__(self): - return "" % ( - self.origin.decode("utf-8", "replace"), - self.field_value.decode("utf-8", "replace"), + return ( + "" % ( + self.origin.decode('utf-8', 'ignore'), + self.field_value.decode('utf-8', 'ignore'), + ) )