Skip to content

Commit 1300507

Browse files
committed
Support RFC 8441 CONNECT requests
This allows the `:protocol` pseudo header field when the method is CONNECT as allowed in RFC 8441 for websocket over HTTP/2 support. Note that this doesn't introduce any extra constraints on CONNECT methods (other than allowing the `:protocol` header) and hence should support any existing functionality.
1 parent cf8a2c5 commit 1300507

File tree

4 files changed

+67
-2
lines changed

4 files changed

+67
-2
lines changed

HISTORY.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ API Changes (Backward-Compatible)
2121
``h2.events.PingAcknowledged`` is deprecated in favour of the identical
2222
``h2.events.PingAckReceived``.
2323
- Added ``ENABLE_CONNECT_PROTOCOL`` to ``h2.settings.SettingCodes``.
24+
- Support ``CONNECT`` requests with a ``:protocol`` pseudo header
25+
thereby supporting RFC 8441.
2426

2527
Bugfixes
2628
~~~~~~~~

h2/utilities.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
b':authority', u':authority',
3434
b':path', u':path',
3535
b':status', u':status',
36+
b':protocol', u':protocol',
3637
])
3738

3839

@@ -47,13 +48,19 @@
4748
b':scheme', u':scheme',
4849
b':path', u':path',
4950
b':authority', u':authority',
50-
b':method', u':method'
51+
b':method', u':method',
52+
b':protocol', u':protocol',
5153
])
5254

5355

5456
_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status'])
5557

5658

59+
# A Set of pseudo headers that are only valid if the method is
60+
# CONNECT, see RFC 8441 § 5
61+
_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol', u':protocol'])
62+
63+
5764
if sys.version_info[0] == 2: # Python 2.X
5865
_WHITESPACE = frozenset(whitespace)
5966
else: # Python 3.3+
@@ -323,6 +330,7 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags):
323330
"""
324331
seen_pseudo_header_fields = set()
325332
seen_regular_header = False
333+
method = None
326334

327335
for header in headers:
328336
if _custom_startswith(header[0], b':', u':'):
@@ -344,18 +352,25 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags):
344352
"Received custom pseudo-header field %s" % header[0]
345353
)
346354

355+
if header[0] in (b':method', u':method'):
356+
if not isinstance(header[1], bytes):
357+
method = header[1].encode('utf-8')
358+
else:
359+
method = header[1]
360+
347361
else:
348362
seen_regular_header = True
349363

350364
yield header
351365

352366
# Check the pseudo-headers we got to confirm they're acceptable.
353367
_check_pseudo_header_field_acceptability(
354-
seen_pseudo_header_fields, hdr_validation_flags
368+
seen_pseudo_header_fields, method, hdr_validation_flags
355369
)
356370

357371

358372
def _check_pseudo_header_field_acceptability(pseudo_headers,
373+
method,
359374
hdr_validation_flags):
360375
"""
361376
Given the set of pseudo-headers present in a header block and the
@@ -394,6 +409,13 @@ def _check_pseudo_header_field_acceptability(pseudo_headers,
394409
"Encountered response-only headers %s" %
395410
invalid_request_headers
396411
)
412+
if method != b'CONNECT':
413+
invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS
414+
if invalid_headers:
415+
raise ProtocolError(
416+
"Encountered connect-request-only headers %s" %
417+
invalid_headers
418+
)
397419

398420

399421
def _validate_host_authority_header(headers):

test/test_invalid_headers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class TestInvalidFrameSequences(object):
5454
base_request_headers + [('name', 'value with trailing space ')],
5555
[header for header in base_request_headers
5656
if header[0] != ':authority'],
57+
[(':protocol', 'websocket')] + base_request_headers,
5758
]
5859
server_config = h2.config.H2Configuration(
5960
client_side=False, header_encoding='utf-8'

test/test_rfc8441.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
test_rfc8441
4+
~~~~~~~~~~~~
5+
6+
Test the RFC 8441 extended connect request support.
7+
"""
8+
import h2.config
9+
import h2.connection
10+
import h2.events
11+
12+
13+
class TestRFC8441(object):
14+
"""
15+
Tests that the client supports sending an extended connect request
16+
and the server supports receiving it.
17+
"""
18+
19+
def test_can_send_headers(self, frame_factory):
20+
headers = [
21+
(b':authority', b'example.com'),
22+
(b':path', b'/'),
23+
(b':scheme', b'https'),
24+
(b':method', b'CONNECT'),
25+
(b':protocol', b'websocket'),
26+
(b'user-agent', b'someua/0.0.1'),
27+
]
28+
29+
client = h2.connection.H2Connection()
30+
client.initiate_connection()
31+
client.send_headers(stream_id=1, headers=headers)
32+
33+
server = h2.connection.H2Connection(
34+
config=h2.config.H2Configuration(client_side=False)
35+
)
36+
events = server.receive_data(client.data_to_send())
37+
event = events[1]
38+
assert isinstance(event, h2.events.RequestReceived)
39+
assert event.stream_id == 1
40+
assert event.headers == headers

0 commit comments

Comments
 (0)