Skip to content

Commit 62b77a1

Browse files
Fixing Issue python-hyper#319 in hyper-h2, adding ability to enable/disable RFC8441 extension through H2Configuration.
1 parent 5bfbb67 commit 62b77a1

File tree

7 files changed

+253
-8
lines changed

7 files changed

+253
-8
lines changed

Diff for: src/h2/config.py

+9
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ class H2Configuration:
129129
normalize_inbound_headers = _BooleanConfigOption(
130130
'normalize_inbound_headers'
131131
)
132+
enable_rfc8441 = _BooleanConfigOption(
133+
'enable_rfc8441'
134+
)
132135

133136
def __init__(self,
134137
client_side=True,
@@ -137,13 +140,15 @@ def __init__(self,
137140
normalize_outbound_headers=True,
138141
validate_inbound_headers=True,
139142
normalize_inbound_headers=True,
143+
enable_rfc8441=False,
140144
logger=None):
141145
self.client_side = client_side
142146
self.header_encoding = header_encoding
143147
self.validate_outbound_headers = validate_outbound_headers
144148
self.normalize_outbound_headers = normalize_outbound_headers
145149
self.validate_inbound_headers = validate_inbound_headers
146150
self.normalize_inbound_headers = normalize_inbound_headers
151+
self.enable_rfc8441 = enable_rfc8441
147152
self.logger = logger or DummyLogger(__name__)
148153

149154
@property
@@ -168,3 +173,7 @@ def header_encoding(self, value):
168173
if value is True:
169174
raise ValueError("header_encoding cannot be True")
170175
self._header_encoding = value
176+
177+
@property
178+
def is_rfc8441_enabled(self):
179+
return self.enable_rfc8441

Diff for: src/h2/connection.py

+2
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ def __init__(self, config=None):
326326
self.DEFAULT_MAX_HEADER_LIST_SIZE,
327327
}
328328
)
329+
if self.config.is_rfc8441_enabled:
330+
self.local_settings.enable_connect_protocol = 1
329331
self.remote_settings = Settings(client=not self.config.client_side)
330332

331333
# The current value of the connection flow control windows on the

Diff for: src/h2/stream.py

+1
Original file line numberDiff line numberDiff line change
@@ -1230,6 +1230,7 @@ def _build_hdr_validation_flags(self, events):
12301230
is_trailer=is_trailer,
12311231
is_response_header=is_response_header,
12321232
is_push_promise=is_push_promise,
1233+
is_rfc8441_enabled=self.config.is_rfc8441_enabled,
12331234
)
12341235

12351236
def _build_headers_frames(self,

Diff for: src/h2/utilities.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def authority_from_headers(headers):
186186
# should be applied to a given set of headers.
187187
HeaderValidationFlags = collections.namedtuple(
188188
'HeaderValidationFlags',
189-
['is_client', 'is_trailer', 'is_response_header', 'is_push_promise']
189+
['is_client', 'is_trailer', 'is_response_header', 'is_push_promise', 'is_rfc8441_enabled']
190190
)
191191

192192

@@ -316,6 +316,18 @@ def _assert_header_in_set(string_header, bytes_header, header_set):
316316
)
317317

318318

319+
def _assert_header_not_in_set(string_header, bytes_header, header_set):
320+
"""
321+
Given a set of header names, checks whether the string or byte version of
322+
the header name is not present. Raises a Protocol error with the appropriate
323+
error if it's present.
324+
"""
325+
if (string_header in header_set or bytes_header in header_set):
326+
raise ProtocolError(
327+
"Header block must not contain %s header" % string_header
328+
)
329+
330+
319331
def _reject_pseudo_header_fields(headers, hdr_validation_flags):
320332
"""
321333
Raises a ProtocolError if duplicate pseudo-header fields are found in a
@@ -396,9 +408,15 @@ def _check_pseudo_header_field_acceptability(pseudo_headers,
396408
not hdr_validation_flags.is_trailer):
397409
# This is a request, so we need to have seen :path, :method, and
398410
# :scheme.
399-
_assert_header_in_set(u':path', b':path', pseudo_headers)
400411
_assert_header_in_set(u':method', b':method', pseudo_headers)
401-
_assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
412+
if method == b'CONNECT':
413+
_assert_header_in_set(u':authority', b':authority', pseudo_headers)
414+
if method == b'CONNECT' and not hdr_validation_flags.is_rfc8441_enabled:
415+
_assert_header_not_in_set(u':path', b':path', pseudo_headers)
416+
_assert_header_not_in_set(u':scheme', b':scheme', pseudo_headers)
417+
else:
418+
_assert_header_in_set(u':path', b':path', pseudo_headers)
419+
_assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
402420
invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS
403421
if invalid_request_headers:
404422
raise ProtocolError(

Diff for: test/test_config.py

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def test_defaults(self):
2222
config = h2.config.H2Configuration()
2323
assert config.client_side
2424
assert config.header_encoding is None
25+
assert config.is_rfc8441_enabled is False
2526
assert isinstance(config.logger, h2.config.DummyLogger)
2627

2728
boolean_config_options = [
@@ -30,6 +31,7 @@ def test_defaults(self):
3031
'normalize_outbound_headers',
3132
'validate_inbound_headers',
3233
'normalize_inbound_headers',
34+
'enable_rfc8441',
3335
]
3436

3537
@pytest.mark.parametrize('option_name', boolean_config_options)
@@ -120,6 +122,15 @@ def test_header_encoding_is_reflected_attr(self, header_encoding):
120122
config.header_encoding = header_encoding
121123
assert config.header_encoding == header_encoding
122124

125+
@pytest.mark.parametrize('enable_rfc8441', [False, True])
126+
def test_header_encoding_is_reflected_init(self, enable_rfc8441):
127+
"""
128+
The value of ``enable_rfc8441``, when set, is reflected in the value
129+
via the initializer.
130+
"""
131+
config = h2.config.H2Configuration(enable_rfc8441=enable_rfc8441)
132+
assert config.is_rfc8441_enabled == enable_rfc8441
133+
123134
def test_logger_instance_is_reflected(self):
124135
"""
125136
The value of ``logger``, when set, is reflected in the value.

Diff for: test/test_invalid_headers.py

+205-3
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,10 @@ class TestFilter(object):
423423

424424
hdr_validation_combos = [
425425
h2.utilities.HeaderValidationFlags(
426-
is_client, is_trailer, is_response_header, is_push_promise
426+
is_client, is_trailer, is_response_header, is_push_promise, is_rfc8441_enabled
427427
)
428-
for is_client, is_trailer, is_response_header, is_push_promise in (
429-
itertools.product([True, False], repeat=4)
428+
for is_client, is_trailer, is_response_header, is_push_promise, is_rfc8441_enabled in (
429+
itertools.product([True, False], repeat=5)
430430
)
431431
]
432432

@@ -494,6 +494,68 @@ class TestFilter(object):
494494
(u':path', u''),
495495
),
496496
)
497+
invalid_connect_request_block_bytes = (
498+
# First, missing :authority with :protocol header
499+
(
500+
(b':method', b'CONNECT'),
501+
(b':protocol', b'test_value'),
502+
(b'host', b'example.com'),
503+
),
504+
# Next, missing :authority without :protocol header
505+
(
506+
(b':method', b'CONNECT'),
507+
(b'host', b'example.com'),
508+
)
509+
)
510+
invalid_connect_request_block_unicode = (
511+
# First, missing :authority with :protocol header
512+
(
513+
(u':method', u'CONNECT'),
514+
(u':protocol', u'websocket'),
515+
(u'host', u'example.com'),
516+
),
517+
# Next, missing :authority without :protocol header
518+
(
519+
(u':method', u'CONNECT'),
520+
(u'host', u'example.com'),
521+
),
522+
)
523+
invalid_connect_req_rfc8441_bytes = (
524+
# First, missing :path header
525+
(
526+
(b':authority', b'example.com'),
527+
(b':method', b'CONNECT'),
528+
(b':protocol', b'test_value'),
529+
(b':scheme', b'https'),
530+
(b'host', b'example.com'),
531+
),
532+
# Next, missing :scheme header
533+
(
534+
(b':authority', b'example.com'),
535+
(b':method', b'CONNECT'),
536+
(b':protocol', b'test_value'),
537+
(b':path', b'/'),
538+
(b'host', b'example.com'),
539+
)
540+
)
541+
invalid_connect_req_rfc8441_unicode = (
542+
# First, missing :path header
543+
(
544+
(u':authority', u'example.com'),
545+
(u':method', u'CONNECT'),
546+
(u':protocol', u'test_value'),
547+
(u':scheme', u'https'),
548+
(u'host', u'example.com'),
549+
),
550+
# Next, missing :scheme header
551+
(
552+
(u':authority', u'example.com'),
553+
(u':method', u'CONNECT'),
554+
(u':protocol', u'test_value'),
555+
(u':path', u'/'),
556+
(u'host', u'example.com'),
557+
)
558+
)
497559

498560
# All headers that are forbidden from either request or response blocks.
499561
forbidden_request_headers_bytes = (b':status',)
@@ -504,6 +566,8 @@ class TestFilter(object):
504566
forbidden_response_headers_unicode = (
505567
u':path', u':scheme', u':authority', u':method'
506568
)
569+
forbidden_connect_request_headers_bytes = (b':scheme', b':path')
570+
forbidden_connect_request_headers_unicode = (u':scheme', u':path')
507571

508572
@pytest.mark.parametrize('validation_function', validation_functions)
509573
@pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos)
@@ -688,6 +752,144 @@ def test_inbound_resp_header_extra_pseudo_headers(self,
688752
with pytest.raises(h2.exceptions.ProtocolError):
689753
list(h2.utilities.validate_headers(headers, hdr_validation_flags))
690754

755+
@pytest.mark.parametrize(
756+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
757+
)
758+
@pytest.mark.parametrize(
759+
'header_block', (
760+
invalid_connect_request_block_bytes +
761+
invalid_connect_request_block_unicode
762+
)
763+
)
764+
def test_outbound_connect_req_missing_pseudo_headers(self,
765+
hdr_validation_flags,
766+
header_block):
767+
if not hdr_validation_flags.is_rfc8441_enabled:
768+
with pytest.raises(h2.exceptions.ProtocolError) as protocol_error:
769+
list(
770+
h2.utilities.validate_outbound_headers(
771+
header_block, hdr_validation_flags
772+
)
773+
)
774+
# Check if missing :path and :scheme headers doesn't throw ProtocolError exception
775+
assert "missing mandatory :path header" not in str(protocol_error.value)
776+
assert "missing mandatory :scheme header" not in str(protocol_error.value)
777+
778+
@pytest.mark.parametrize(
779+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
780+
)
781+
@pytest.mark.parametrize(
782+
'header_block', invalid_connect_request_block_bytes
783+
)
784+
def test_inbound_connect_req_missing_pseudo_headers(self,
785+
hdr_validation_flags,
786+
header_block):
787+
if not hdr_validation_flags.is_rfc8441_enabled:
788+
with pytest.raises(h2.exceptions.ProtocolError) as protocol_error:
789+
list(
790+
h2.utilities.validate_headers(
791+
header_block, hdr_validation_flags
792+
)
793+
)
794+
# Check if missing :path and :scheme headers doesn't throw ProtocolError exception
795+
assert "missing mandatory :path header" not in str(protocol_error.value)
796+
assert "missing mandatory :scheme header" not in str(protocol_error.value)
797+
798+
@pytest.mark.parametrize(
799+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
800+
)
801+
@pytest.mark.parametrize(
802+
'invalid_header',
803+
forbidden_connect_request_headers_bytes + forbidden_connect_request_headers_unicode
804+
)
805+
def test_outbound_connect_req_extra_pseudo_headers(self,
806+
hdr_validation_flags,
807+
invalid_header):
808+
"""
809+
Inbound request header blocks containing the forbidden request headers
810+
fail validation.
811+
"""
812+
headers = [
813+
(b':authority', b'google.com'),
814+
(b':method', b'CONNECT'),
815+
(b':protocol', b'websocket'),
816+
]
817+
if not hdr_validation_flags.is_rfc8441_enabled:
818+
headers.append((invalid_header, b'some value'))
819+
with pytest.raises(h2.exceptions.ProtocolError) as protocol_error:
820+
list(h2.utilities.validate_outbound_headers(headers, hdr_validation_flags))
821+
if isinstance(invalid_header, bytes):
822+
expected_exception_string = (b'Header block must not contain ' + invalid_header + b' header')\
823+
.decode("utf-8")
824+
else:
825+
expected_exception_string = 'Header block must not contain ' + invalid_header + ' header'
826+
assert expected_exception_string == str(protocol_error.value)
827+
828+
@pytest.mark.parametrize(
829+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
830+
)
831+
@pytest.mark.parametrize(
832+
'invalid_header',
833+
forbidden_connect_request_headers_bytes
834+
)
835+
def test_inbound_connect_req_extra_pseudo_headers(self,
836+
hdr_validation_flags,
837+
invalid_header):
838+
"""
839+
Inbound request header blocks containing the forbidden request headers
840+
fail validation.
841+
"""
842+
headers = [
843+
(b':authority', b'google.com'),
844+
(b':method', b'CONNECT'),
845+
(b':protocol', b'some value'),
846+
]
847+
if not hdr_validation_flags.is_rfc8441_enabled:
848+
headers.append((invalid_header, b'some value'))
849+
with pytest.raises(h2.exceptions.ProtocolError) as protocol_error:
850+
list(h2.utilities.validate_headers(headers, hdr_validation_flags))
851+
assert (b'Header block must not contain ' + invalid_header + b' header').decode("utf-8") \
852+
== str(protocol_error.value)
853+
854+
855+
@pytest.mark.parametrize(
856+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
857+
)
858+
@pytest.mark.parametrize(
859+
'header_block', (
860+
invalid_connect_req_rfc8441_bytes +
861+
invalid_connect_req_rfc8441_unicode
862+
)
863+
)
864+
def test_outbound_connect_req_rfc8441_missing_pseudo_headers(self,
865+
hdr_validation_flags,
866+
header_block):
867+
if hdr_validation_flags.is_rfc8441_enabled:
868+
with pytest.raises(h2.exceptions.ProtocolError):
869+
list(
870+
h2.utilities.validate_outbound_headers(
871+
header_block, hdr_validation_flags
872+
)
873+
)
874+
875+
@pytest.mark.parametrize(
876+
'hdr_validation_flags', hdr_validation_request_headers_no_trailer
877+
)
878+
@pytest.mark.parametrize(
879+
'header_block', invalid_connect_req_rfc8441_bytes
880+
)
881+
def test_inbound_connect_req_rfc8441_missing_pseudo_headers(self,
882+
hdr_validation_flags,
883+
header_block):
884+
if hdr_validation_flags.is_rfc8441_enabled:
885+
print("here", header_block)
886+
with pytest.raises(h2.exceptions.ProtocolError):
887+
list(
888+
h2.utilities.validate_headers(
889+
header_block, hdr_validation_flags
890+
)
891+
)
892+
691893

692894
class TestOversizedHeaders(object):
693895
"""

Diff for: test/test_rfc8441.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ def test_can_send_headers(self, frame_factory):
2626
(b'user-agent', b'someua/0.0.1'),
2727
]
2828

29-
client = h2.connection.H2Connection()
29+
client = h2.connection.H2Connection(
30+
config=h2.config.H2Configuration(enable_rfc8441=True)
31+
)
3032
client.initiate_connection()
3133
client.send_headers(stream_id=1, headers=headers)
3234

3335
server = h2.connection.H2Connection(
34-
config=h2.config.H2Configuration(client_side=False)
36+
config=h2.config.H2Configuration(client_side=False, enable_rfc8441=True)
3537
)
3638
events = server.receive_data(client.data_to_send())
3739
event = events[1]

0 commit comments

Comments
 (0)