Skip to content

Commit 6134dd4

Browse files
Fixing issue python-hyper#319, adding option to enable/disable RFC8441 ext. through H2Configuration
1 parent 5bfbb67 commit 6134dd4

File tree

7 files changed

+284
-8
lines changed

7 files changed

+284
-8
lines changed

src/h2/config.py

Lines changed: 9 additions & 0 deletions
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

src/h2/connection.py

Lines changed: 2 additions & 0 deletions
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

src/h2/stream.py

Lines changed: 1 addition & 0 deletions
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,

src/h2/utilities.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,13 @@ 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+
[
190+
'is_client',
191+
'is_trailer',
192+
'is_response_header',
193+
'is_push_promise',
194+
'is_rfc8441_enabled',
195+
]
190196
)
191197

192198

@@ -316,6 +322,18 @@ def _assert_header_in_set(string_header, bytes_header, header_set):
316322
)
317323

318324

325+
def _assert_header_not_in_set(string_header, bytes_header, header_set):
326+
"""
327+
Given a set of header names, checks whether the string or byte version of
328+
the header name is not present. Raises a Protocol error with the
329+
appropriate error if it's present.
330+
"""
331+
if (string_header in header_set or bytes_header in header_set):
332+
raise ProtocolError(
333+
"Header block must not contain %s header" % string_header
334+
)
335+
336+
319337
def _reject_pseudo_header_fields(headers, hdr_validation_flags):
320338
"""
321339
Raises a ProtocolError if duplicate pseudo-header fields are found in a
@@ -396,9 +414,16 @@ def _check_pseudo_header_field_acceptability(pseudo_headers,
396414
not hdr_validation_flags.is_trailer):
397415
# This is a request, so we need to have seen :path, :method, and
398416
# :scheme.
399-
_assert_header_in_set(u':path', b':path', pseudo_headers)
400417
_assert_header_in_set(u':method', b':method', pseudo_headers)
401-
_assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
418+
if method == b'CONNECT':
419+
_assert_header_in_set(u':authority', b':authority', pseudo_headers)
420+
if method == b'CONNECT' and \
421+
not hdr_validation_flags.is_rfc8441_enabled:
422+
_assert_header_not_in_set(u':path', b':path', pseudo_headers)
423+
_assert_header_not_in_set(u':scheme', b':scheme', pseudo_headers)
424+
else:
425+
_assert_header_in_set(u':path', b':path', pseudo_headers)
426+
_assert_header_in_set(u':scheme', b':scheme', pseudo_headers)
402427
invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS
403428
if invalid_request_headers:
404429
raise ProtocolError(

test/test_config.py

Lines changed: 11 additions & 0 deletions
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_enable_rfc8441_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.

test/test_invalid_headers.py

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,15 @@ 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,
427+
is_trailer,
428+
is_response_header,
429+
is_push_promise,
430+
is_rfc8441_enabled,
427431
)
428-
for is_client, is_trailer, is_response_header, is_push_promise in (
429-
itertools.product([True, False], repeat=4)
432+
for is_client, is_trailer, is_response_header, is_push_promise,
433+
is_rfc8441_enabled in (
434+
itertools.product([True, False], repeat=5)
430435
)
431436
]
432437

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

498565
# All headers that are forbidden from either request or response blocks.
499566
forbidden_request_headers_bytes = (b':status',)
@@ -504,6 +571,8 @@ class TestFilter(object):
504571
forbidden_response_headers_unicode = (
505572
u':path', u':scheme', u':authority', u':method'
506573
)
574+
forbidden_connect_request_headers_bytes = (b':scheme', b':path')
575+
forbidden_connect_request_headers_unicode = (u':scheme', u':path')
507576

508577
@pytest.mark.parametrize('validation_function', validation_functions)
509578
@pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos)
@@ -688,6 +757,161 @@ def test_inbound_resp_header_extra_pseudo_headers(self,
688757
with pytest.raises(h2.exceptions.ProtocolError):
689758
list(h2.utilities.validate_headers(headers, hdr_validation_flags))
690759

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

692916
class TestOversizedHeaders(object):
693917
"""

0 commit comments

Comments
 (0)