tigrcorn-protocols 0.3.16.dev5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrcorn_protocols/__init__.py +1 -0
- tigrcorn_protocols/_compression.py +219 -0
- tigrcorn_protocols/connect.py +107 -0
- tigrcorn_protocols/content_coding.py +179 -0
- tigrcorn_protocols/custom/__init__.py +3 -0
- tigrcorn_protocols/custom/adapters.py +18 -0
- tigrcorn_protocols/custom/registry.py +15 -0
- tigrcorn_protocols/flow/__init__.py +1 -0
- tigrcorn_protocols/flow/backpressure.py +17 -0
- tigrcorn_protocols/flow/buffers.py +29 -0
- tigrcorn_protocols/flow/credits.py +21 -0
- tigrcorn_protocols/flow/keepalive.py +85 -0
- tigrcorn_protocols/flow/timeouts.py +17 -0
- tigrcorn_protocols/flow/watermarks.py +16 -0
- tigrcorn_protocols/http1/__init__.py +16 -0
- tigrcorn_protocols/http1/keepalive.py +21 -0
- tigrcorn_protocols/http1/parser.py +481 -0
- tigrcorn_protocols/http1/serializer.py +198 -0
- tigrcorn_protocols/http1/state.py +9 -0
- tigrcorn_protocols/http2/__init__.py +16 -0
- tigrcorn_protocols/http2/codec.py +266 -0
- tigrcorn_protocols/http2/flow.py +35 -0
- tigrcorn_protocols/http2/handler.py +1303 -0
- tigrcorn_protocols/http2/hpack.py +393 -0
- tigrcorn_protocols/http2/state.py +226 -0
- tigrcorn_protocols/http2/streams.py +76 -0
- tigrcorn_protocols/http2/websocket.py +360 -0
- tigrcorn_protocols/http3/__init__.py +82 -0
- tigrcorn_protocols/http3/codec.py +148 -0
- tigrcorn_protocols/http3/handler/__init__.py +3 -0
- tigrcorn_protocols/http3/handler/core.py +1823 -0
- tigrcorn_protocols/http3/handler/webtransport.py +184 -0
- tigrcorn_protocols/http3/handler.py +3 -0
- tigrcorn_protocols/http3/qpack.py +843 -0
- tigrcorn_protocols/http3/state.py +129 -0
- tigrcorn_protocols/http3/streams.py +657 -0
- tigrcorn_protocols/http3/websocket.py +360 -0
- tigrcorn_protocols/lifespan/__init__.py +3 -0
- tigrcorn_protocols/lifespan/driver.py +83 -0
- tigrcorn_protocols/py.typed +1 -0
- tigrcorn_protocols/rawframed/__init__.py +5 -0
- tigrcorn_protocols/rawframed/codec.py +18 -0
- tigrcorn_protocols/rawframed/frames.py +28 -0
- tigrcorn_protocols/rawframed/handler.py +72 -0
- tigrcorn_protocols/rawframed/state.py +9 -0
- tigrcorn_protocols/registry.py +22 -0
- tigrcorn_protocols/scheduler/__init__.py +17 -0
- tigrcorn_protocols/scheduler/cancellation.py +40 -0
- tigrcorn_protocols/scheduler/dispatch.py +27 -0
- tigrcorn_protocols/scheduler/fairness.py +21 -0
- tigrcorn_protocols/scheduler/policy.py +12 -0
- tigrcorn_protocols/scheduler/priorities.py +8 -0
- tigrcorn_protocols/scheduler/quotas.py +19 -0
- tigrcorn_protocols/scheduler/runtime.py +156 -0
- tigrcorn_protocols/scheduler/tasks.py +31 -0
- tigrcorn_protocols/sessions/__init__.py +1 -0
- tigrcorn_protocols/sessions/base.py +16 -0
- tigrcorn_protocols/sessions/connection.py +12 -0
- tigrcorn_protocols/sessions/limits.py +12 -0
- tigrcorn_protocols/sessions/manager.py +31 -0
- tigrcorn_protocols/sessions/metadata.py +10 -0
- tigrcorn_protocols/sessions/quic.py +14 -0
- tigrcorn_protocols/streams/__init__.py +1 -0
- tigrcorn_protocols/streams/base.py +13 -0
- tigrcorn_protocols/streams/ids.py +5 -0
- tigrcorn_protocols/streams/multiplex.py +6 -0
- tigrcorn_protocols/streams/registry.py +22 -0
- tigrcorn_protocols/streams/singleplex.py +6 -0
- tigrcorn_protocols/websocket/__init__.py +1 -0
- tigrcorn_protocols/websocket/codec.py +31 -0
- tigrcorn_protocols/websocket/extensions.py +324 -0
- tigrcorn_protocols/websocket/frames.py +174 -0
- tigrcorn_protocols/websocket/handler.py +462 -0
- tigrcorn_protocols/websocket/handshake.py +66 -0
- tigrcorn_protocols/websocket/state.py +10 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from tigrcorn_core.errors import ProtocolError
|
|
6
|
+
from tigrcorn_protocols.http3.codec import (
|
|
7
|
+
FRAME_CANCEL_PUSH,
|
|
8
|
+
FRAME_DATA,
|
|
9
|
+
FRAME_GOAWAY,
|
|
10
|
+
FRAME_HEADERS,
|
|
11
|
+
FRAME_MAX_PUSH_ID,
|
|
12
|
+
FRAME_PUSH_PROMISE,
|
|
13
|
+
FRAME_SETTINGS,
|
|
14
|
+
H3_CLOSED_CRITICAL_STREAM,
|
|
15
|
+
H3_EXCESSIVE_LOAD,
|
|
16
|
+
H3_FRAME_ERROR,
|
|
17
|
+
H3_FRAME_UNEXPECTED,
|
|
18
|
+
H3_GENERAL_PROTOCOL_ERROR,
|
|
19
|
+
H3_ID_ERROR,
|
|
20
|
+
H3_MESSAGE_ERROR,
|
|
21
|
+
H3_MISSING_SETTINGS,
|
|
22
|
+
H3_REQUEST_INCOMPLETE,
|
|
23
|
+
H3_REQUEST_REJECTED,
|
|
24
|
+
H3_SETTINGS_ERROR,
|
|
25
|
+
H3_STREAM_CREATION_ERROR,
|
|
26
|
+
HTTP3ConnectionError,
|
|
27
|
+
HTTP3StreamError,
|
|
28
|
+
QPACK_DECODER_STREAM_ERROR,
|
|
29
|
+
QPACK_DECOMPRESSION_FAILED,
|
|
30
|
+
QPACK_ENCODER_STREAM_ERROR,
|
|
31
|
+
STREAM_TYPE_CONTROL,
|
|
32
|
+
decode_frame,
|
|
33
|
+
decode_settings,
|
|
34
|
+
decode_single_varint,
|
|
35
|
+
encode_frame,
|
|
36
|
+
encode_settings,
|
|
37
|
+
)
|
|
38
|
+
from tigrcorn_protocols.http3.qpack import (
|
|
39
|
+
QpackBlocked,
|
|
40
|
+
QpackDecoder,
|
|
41
|
+
QpackDecoderStreamError,
|
|
42
|
+
QpackDecompressionFailed,
|
|
43
|
+
QpackEncoder,
|
|
44
|
+
QpackEncoderStreamError,
|
|
45
|
+
decode_field_section,
|
|
46
|
+
encode_field_section,
|
|
47
|
+
)
|
|
48
|
+
from tigrcorn_protocols.http3.state import (
|
|
49
|
+
HTTP3BlockedSection,
|
|
50
|
+
HTTP3ConnectionState,
|
|
51
|
+
HTTP3PushPromiseState,
|
|
52
|
+
HTTP3RequestPhase_DATA,
|
|
53
|
+
HTTP3RequestPhase_INITIAL,
|
|
54
|
+
HTTP3RequestPhase_TRAILERS,
|
|
55
|
+
HTTP3RequestState,
|
|
56
|
+
HTTP3UniStreamState,
|
|
57
|
+
)
|
|
58
|
+
from tigrcorn_core.utils.bytes import decode_quic_varint, encode_quic_varint
|
|
59
|
+
|
|
60
|
+
HTTP3_STREAM_PRESSURE_CERTIFICATION_SCOPES: tuple[str, ...] = ('stream-level-backpressure', 'connection-level-backpressure', 'goaway-pressure')
|
|
61
|
+
DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT = 65_536
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def supported_http3_stream_pressure_certification_scopes() -> tuple[str, ...]:
|
|
65
|
+
return HTTP3_STREAM_PRESSURE_CERTIFICATION_SCOPES
|
|
66
|
+
|
|
67
|
+
STREAM_TYPE_PUSH = 0x01
|
|
68
|
+
STREAM_TYPE_QPACK_ENCODER = 0x02
|
|
69
|
+
STREAM_TYPE_QPACK_DECODER = 0x03
|
|
70
|
+
SETTING_QPACK_MAX_TABLE_CAPACITY = 0x01
|
|
71
|
+
SETTING_MAX_FIELD_SECTION_SIZE = 0x06
|
|
72
|
+
SETTING_QPACK_BLOCKED_STREAMS = 0x07
|
|
73
|
+
_REQUEST_STATE_INITIAL = HTTP3RequestPhase_INITIAL
|
|
74
|
+
_REQUEST_STATE_DATA = HTTP3RequestPhase_DATA
|
|
75
|
+
_REQUEST_STATE_TRAILERS = HTTP3RequestPhase_TRAILERS
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _header_section_size(headers: list[tuple[bytes, bytes]]) -> int:
|
|
79
|
+
return sum(len(name) + len(value) + 32 for name, value in headers)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_content_length(headers: list[tuple[bytes, bytes]], *, stream_id: int) -> int | None:
|
|
84
|
+
values: list[bytes] = []
|
|
85
|
+
for name, value in headers:
|
|
86
|
+
if name.lower() != b'content-length':
|
|
87
|
+
continue
|
|
88
|
+
for part in value.split(b','):
|
|
89
|
+
values.append(part.strip())
|
|
90
|
+
if not values:
|
|
91
|
+
return None
|
|
92
|
+
parsed: int | None = None
|
|
93
|
+
for value in values:
|
|
94
|
+
if not value or not value.isdigit():
|
|
95
|
+
raise HTTP3StreamError('invalid content-length header', error_code=H3_MESSAGE_ERROR, stream_id=stream_id)
|
|
96
|
+
current = int(value)
|
|
97
|
+
if parsed is None:
|
|
98
|
+
parsed = current
|
|
99
|
+
continue
|
|
100
|
+
if parsed != current:
|
|
101
|
+
raise HTTP3StreamError('conflicting content-length values', error_code=H3_MESSAGE_ERROR, stream_id=stream_id)
|
|
102
|
+
return parsed
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _extract_status_code(headers: list[tuple[bytes, bytes]]) -> int | None:
|
|
106
|
+
for name, value in headers:
|
|
107
|
+
if name != b':status':
|
|
108
|
+
continue
|
|
109
|
+
if not value.isdigit():
|
|
110
|
+
return None
|
|
111
|
+
return int(value)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _control_sender_is_client(stream_id: int) -> bool:
|
|
116
|
+
return (stream_id & 0x01) == 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(slots=True)
|
|
120
|
+
class HTTP3RequestStream:
|
|
121
|
+
state: HTTP3RequestState
|
|
122
|
+
qpack_encoder: QpackEncoder | None = None
|
|
123
|
+
qpack_decoder: QpackDecoder | None = None
|
|
124
|
+
connection_state: HTTP3ConnectionState | None = None
|
|
125
|
+
role: str | None = None
|
|
126
|
+
parse_buffer_limit: int = DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT
|
|
127
|
+
|
|
128
|
+
def encode_request(self, headers: list[tuple[bytes, bytes]], body: bytes = b'') -> bytes:
|
|
129
|
+
raw = bytearray()
|
|
130
|
+
if self.qpack_encoder is not None:
|
|
131
|
+
header_block = self.qpack_encoder.encode_field_section(headers, stream_id=self.state.stream_id)
|
|
132
|
+
else:
|
|
133
|
+
header_block = encode_field_section(headers)
|
|
134
|
+
raw.extend(encode_frame(FRAME_HEADERS, header_block))
|
|
135
|
+
if body:
|
|
136
|
+
raw.extend(encode_frame(FRAME_DATA, body))
|
|
137
|
+
return bytes(raw)
|
|
138
|
+
|
|
139
|
+
def _max_field_section_size(self) -> int | None:
|
|
140
|
+
if self.connection_state is None:
|
|
141
|
+
return None
|
|
142
|
+
limit = self.connection_state.local_settings.get(SETTING_MAX_FIELD_SECTION_SIZE)
|
|
143
|
+
if limit is None or limit <= 0:
|
|
144
|
+
return None
|
|
145
|
+
return limit
|
|
146
|
+
|
|
147
|
+
def _decode_field_section_payload(self, payload: bytes) -> list[tuple[bytes, bytes]]:
|
|
148
|
+
if self.qpack_decoder is None:
|
|
149
|
+
return decode_field_section(payload)
|
|
150
|
+
try:
|
|
151
|
+
field_section = self.qpack_decoder.decode_field_section(payload, stream_id=self.state.stream_id)
|
|
152
|
+
except QpackBlocked as exc:
|
|
153
|
+
raise exc
|
|
154
|
+
except QpackDecompressionFailed as exc:
|
|
155
|
+
raise HTTP3ConnectionError('invalid HTTP/3 field section', error_code=QPACK_DECOMPRESSION_FAILED) from exc
|
|
156
|
+
except ProtocolError as exc:
|
|
157
|
+
raise HTTP3ConnectionError('invalid HTTP/3 field section', error_code=QPACK_DECOMPRESSION_FAILED) from exc
|
|
158
|
+
return field_section.headers
|
|
159
|
+
|
|
160
|
+
def _queue_blocked_section(self, *, kind: str, payload: bytes, push_id: int | None = None) -> None:
|
|
161
|
+
self.state.blocked_header_sections.append(HTTP3BlockedSection(kind=kind, payload=payload, push_id=push_id))
|
|
162
|
+
|
|
163
|
+
def _enforce_parse_buffer_limit(self) -> None:
|
|
164
|
+
if self.parse_buffer_limit <= 0:
|
|
165
|
+
return
|
|
166
|
+
observed = len(self.state.parse_buffer)
|
|
167
|
+
if observed <= self.parse_buffer_limit:
|
|
168
|
+
return
|
|
169
|
+
self.abandon()
|
|
170
|
+
raise HTTP3StreamError(
|
|
171
|
+
'HTTP/3 request stream parse buffer limit exceeded',
|
|
172
|
+
error_code=H3_EXCESSIVE_LOAD,
|
|
173
|
+
stream_id=self.state.stream_id,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _enforce_field_section_size(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
177
|
+
limit = self._max_field_section_size()
|
|
178
|
+
if limit is None:
|
|
179
|
+
return
|
|
180
|
+
if _header_section_size(headers) > limit:
|
|
181
|
+
raise HTTP3StreamError(
|
|
182
|
+
'HTTP/3 field section exceeds advertised size',
|
|
183
|
+
error_code=H3_MESSAGE_ERROR,
|
|
184
|
+
stream_id=self.state.stream_id,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _apply_initial_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
188
|
+
self._enforce_field_section_size(headers)
|
|
189
|
+
status_code = _extract_status_code(headers)
|
|
190
|
+
if self.role == 'client' and status_code is not None and 100 <= status_code < 200:
|
|
191
|
+
self.state.informational_headers.append(list(headers))
|
|
192
|
+
return
|
|
193
|
+
self.state.headers.extend(headers)
|
|
194
|
+
self.state.received_initial_headers = True
|
|
195
|
+
self.state.phase = _REQUEST_STATE_DATA
|
|
196
|
+
content_length = _parse_content_length(headers, stream_id=self.state.stream_id)
|
|
197
|
+
if content_length is not None:
|
|
198
|
+
self.state.expected_content_length = content_length
|
|
199
|
+
if self.state.received_content_length > content_length:
|
|
200
|
+
raise HTTP3StreamError(
|
|
201
|
+
'request body exceeds content-length',
|
|
202
|
+
error_code=H3_MESSAGE_ERROR,
|
|
203
|
+
stream_id=self.state.stream_id,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _apply_trailers(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
207
|
+
self._enforce_field_section_size(headers)
|
|
208
|
+
for name, _value in headers:
|
|
209
|
+
if name.startswith(b':'):
|
|
210
|
+
raise HTTP3StreamError(
|
|
211
|
+
'pseudo-header field in trailer section',
|
|
212
|
+
error_code=H3_MESSAGE_ERROR,
|
|
213
|
+
stream_id=self.state.stream_id,
|
|
214
|
+
)
|
|
215
|
+
self.state.trailers.extend(headers)
|
|
216
|
+
self.state.received_trailers = True
|
|
217
|
+
self.state.phase = _REQUEST_STATE_TRAILERS
|
|
218
|
+
|
|
219
|
+
def _store_push_promise(self, push_id: int, headers: list[tuple[bytes, bytes]]) -> None:
|
|
220
|
+
connection_state = self.connection_state
|
|
221
|
+
if connection_state is None:
|
|
222
|
+
connection_state = HTTP3ConnectionState()
|
|
223
|
+
self.connection_state = connection_state
|
|
224
|
+
max_push_id = connection_state.local_max_push_id
|
|
225
|
+
if max_push_id is None or push_id > max_push_id:
|
|
226
|
+
raise HTTP3ConnectionError('PUSH_PROMISE exceeds advertised MAX_PUSH_ID', error_code=H3_ID_ERROR)
|
|
227
|
+
existing = connection_state.promised_pushes.get(push_id)
|
|
228
|
+
if existing is not None:
|
|
229
|
+
if existing.headers != headers:
|
|
230
|
+
raise HTTP3ConnectionError(
|
|
231
|
+
'inconsistent duplicate PUSH_PROMISE field section',
|
|
232
|
+
error_code=H3_GENERAL_PROTOCOL_ERROR,
|
|
233
|
+
)
|
|
234
|
+
existing.request_stream_ids.add(self.state.stream_id)
|
|
235
|
+
self.state.push_promises[push_id] = existing
|
|
236
|
+
return
|
|
237
|
+
promise = HTTP3PushPromiseState(push_id=push_id, headers=list(headers), request_stream_ids={self.state.stream_id})
|
|
238
|
+
connection_state.promised_pushes[push_id] = promise
|
|
239
|
+
self.state.push_promises[push_id] = promise
|
|
240
|
+
|
|
241
|
+
def _apply_blocked_section(self, section: HTTP3BlockedSection) -> None:
|
|
242
|
+
try:
|
|
243
|
+
headers = self._decode_field_section_payload(section.payload)
|
|
244
|
+
except QpackBlocked:
|
|
245
|
+
raise
|
|
246
|
+
if section.kind == 'initial':
|
|
247
|
+
self._apply_initial_headers(headers)
|
|
248
|
+
return
|
|
249
|
+
if section.kind == 'trailers':
|
|
250
|
+
self._apply_trailers(headers)
|
|
251
|
+
return
|
|
252
|
+
if section.kind == 'push':
|
|
253
|
+
assert section.push_id is not None
|
|
254
|
+
self._store_push_promise(section.push_id, headers)
|
|
255
|
+
return
|
|
256
|
+
raise HTTP3ConnectionError('unknown blocked header section kind', error_code=H3_GENERAL_PROTOCOL_ERROR)
|
|
257
|
+
|
|
258
|
+
def _decode_or_block(self, *, kind: str, payload: bytes, push_id: int | None = None) -> bool:
|
|
259
|
+
try:
|
|
260
|
+
headers = self._decode_field_section_payload(payload)
|
|
261
|
+
except QpackBlocked:
|
|
262
|
+
self._queue_blocked_section(kind=kind, payload=payload, push_id=push_id)
|
|
263
|
+
return True
|
|
264
|
+
if kind == 'initial':
|
|
265
|
+
self._apply_initial_headers(headers)
|
|
266
|
+
return False
|
|
267
|
+
if kind == 'trailers':
|
|
268
|
+
self._apply_trailers(headers)
|
|
269
|
+
return False
|
|
270
|
+
if kind == 'push':
|
|
271
|
+
assert push_id is not None
|
|
272
|
+
self._store_push_promise(push_id, headers)
|
|
273
|
+
return False
|
|
274
|
+
raise HTTP3ConnectionError('unknown header section kind', error_code=H3_GENERAL_PROTOCOL_ERROR)
|
|
275
|
+
|
|
276
|
+
def _handle_headers_frame(self, payload: bytes) -> bool:
|
|
277
|
+
if self.state.phase == _REQUEST_STATE_INITIAL:
|
|
278
|
+
return self._decode_or_block(kind='initial', payload=payload)
|
|
279
|
+
if self.state.phase == _REQUEST_STATE_DATA:
|
|
280
|
+
return self._decode_or_block(kind='trailers', payload=payload)
|
|
281
|
+
raise HTTP3ConnectionError('HEADERS after trailer section', error_code=H3_FRAME_UNEXPECTED)
|
|
282
|
+
|
|
283
|
+
def _handle_data_frame(self, payload: bytes) -> bool:
|
|
284
|
+
if self.state.phase == _REQUEST_STATE_INITIAL:
|
|
285
|
+
raise HTTP3ConnectionError('DATA frame before initial HEADERS', error_code=H3_FRAME_UNEXPECTED)
|
|
286
|
+
if self.state.phase == _REQUEST_STATE_TRAILERS:
|
|
287
|
+
raise HTTP3ConnectionError('DATA frame after trailing HEADERS', error_code=H3_FRAME_UNEXPECTED)
|
|
288
|
+
self.state.body_parts.append(payload)
|
|
289
|
+
self.state.received_content_length += len(payload)
|
|
290
|
+
expected = self.state.expected_content_length
|
|
291
|
+
if expected is not None and self.state.received_content_length > expected:
|
|
292
|
+
raise HTTP3StreamError(
|
|
293
|
+
'request body exceeds content-length',
|
|
294
|
+
error_code=H3_MESSAGE_ERROR,
|
|
295
|
+
stream_id=self.state.stream_id,
|
|
296
|
+
)
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def _handle_push_promise_frame(self, payload: bytes) -> bool:
|
|
300
|
+
if self.role == 'server':
|
|
301
|
+
raise HTTP3ConnectionError('server received PUSH_PROMISE on request stream', error_code=H3_FRAME_UNEXPECTED)
|
|
302
|
+
try:
|
|
303
|
+
push_id, offset = decode_quic_varint(payload, 0)
|
|
304
|
+
except ProtocolError as exc:
|
|
305
|
+
raise HTTP3ConnectionError('malformed PUSH_PROMISE frame payload', error_code=H3_FRAME_ERROR) from exc
|
|
306
|
+
field_section = payload[offset:]
|
|
307
|
+
return self._decode_or_block(kind='push', payload=field_section, push_id=push_id)
|
|
308
|
+
|
|
309
|
+
def _handle_frame(self, frame_type: int, payload: bytes) -> bool:
|
|
310
|
+
if frame_type == FRAME_HEADERS:
|
|
311
|
+
return self._handle_headers_frame(payload)
|
|
312
|
+
if frame_type == FRAME_DATA:
|
|
313
|
+
return self._handle_data_frame(payload)
|
|
314
|
+
if frame_type == FRAME_PUSH_PROMISE:
|
|
315
|
+
return self._handle_push_promise_frame(payload)
|
|
316
|
+
if frame_type in {FRAME_CANCEL_PUSH, FRAME_SETTINGS, FRAME_GOAWAY, FRAME_MAX_PUSH_ID}:
|
|
317
|
+
raise HTTP3ConnectionError('frame not permitted on request stream', error_code=H3_FRAME_UNEXPECTED)
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
def _process_parse_buffer(self) -> None:
|
|
321
|
+
self._enforce_parse_buffer_limit()
|
|
322
|
+
offset = 0
|
|
323
|
+
data = bytes(self.state.parse_buffer)
|
|
324
|
+
while offset < len(data):
|
|
325
|
+
try:
|
|
326
|
+
frame, next_offset = decode_frame(data, offset)
|
|
327
|
+
except ProtocolError:
|
|
328
|
+
break
|
|
329
|
+
offset = next_offset
|
|
330
|
+
blocked = self._handle_frame(frame.frame_type, frame.payload)
|
|
331
|
+
if blocked:
|
|
332
|
+
break
|
|
333
|
+
remaining = data[offset:]
|
|
334
|
+
self.state.parse_buffer.clear()
|
|
335
|
+
self.state.parse_buffer.extend(remaining)
|
|
336
|
+
self._enforce_parse_buffer_limit()
|
|
337
|
+
|
|
338
|
+
def _finalize_complete_message(self) -> None:
|
|
339
|
+
if not self.state.ended:
|
|
340
|
+
return
|
|
341
|
+
if self.state.blocked_header_sections:
|
|
342
|
+
return
|
|
343
|
+
if self.state.parse_buffer:
|
|
344
|
+
raise HTTP3StreamError(
|
|
345
|
+
'request stream ended with incomplete frame',
|
|
346
|
+
error_code=H3_REQUEST_INCOMPLETE,
|
|
347
|
+
stream_id=self.state.stream_id,
|
|
348
|
+
)
|
|
349
|
+
if not self.state.received_initial_headers:
|
|
350
|
+
raise HTTP3StreamError(
|
|
351
|
+
'request stream ended before initial HEADERS',
|
|
352
|
+
error_code=H3_REQUEST_INCOMPLETE,
|
|
353
|
+
stream_id=self.state.stream_id,
|
|
354
|
+
)
|
|
355
|
+
expected = self.state.expected_content_length
|
|
356
|
+
if expected is not None and self.state.received_content_length != expected:
|
|
357
|
+
raise HTTP3StreamError(
|
|
358
|
+
'content-length does not match DATA frame lengths',
|
|
359
|
+
error_code=H3_MESSAGE_ERROR,
|
|
360
|
+
stream_id=self.state.stream_id,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def retry_blocked(self) -> bool:
|
|
364
|
+
if self.qpack_decoder is None or not self.state.blocked_header_sections:
|
|
365
|
+
self._finalize_complete_message()
|
|
366
|
+
return False
|
|
367
|
+
progressed = False
|
|
368
|
+
remaining: list[HTTP3BlockedSection] = []
|
|
369
|
+
for section in self.state.blocked_header_sections:
|
|
370
|
+
try:
|
|
371
|
+
self._apply_blocked_section(section)
|
|
372
|
+
except QpackBlocked:
|
|
373
|
+
remaining.append(section)
|
|
374
|
+
continue
|
|
375
|
+
progressed = True
|
|
376
|
+
self.state.blocked_header_sections = remaining
|
|
377
|
+
if progressed and not self.state.blocked_header_sections and self.state.parse_buffer:
|
|
378
|
+
self._process_parse_buffer()
|
|
379
|
+
self._finalize_complete_message()
|
|
380
|
+
return progressed
|
|
381
|
+
|
|
382
|
+
def abandon(self) -> None:
|
|
383
|
+
if self.state.abandoned:
|
|
384
|
+
return
|
|
385
|
+
self.state.abandoned = True
|
|
386
|
+
if self.qpack_decoder is not None and self.state.blocked_header_sections:
|
|
387
|
+
self.qpack_decoder.cancel_stream(self.state.stream_id)
|
|
388
|
+
self.state.blocked_header_sections.clear()
|
|
389
|
+
self.state.parse_buffer.clear()
|
|
390
|
+
|
|
391
|
+
def receive(self, payload: bytes, *, fin: bool = False) -> HTTP3RequestState:
|
|
392
|
+
if self.state.abandoned:
|
|
393
|
+
return self.state
|
|
394
|
+
self.state.parse_buffer.extend(payload)
|
|
395
|
+
self._enforce_parse_buffer_limit()
|
|
396
|
+
if fin:
|
|
397
|
+
self.state.ended = True
|
|
398
|
+
self._process_parse_buffer()
|
|
399
|
+
self.retry_blocked()
|
|
400
|
+
self._finalize_complete_message()
|
|
401
|
+
return self.state
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@dataclass(slots=True)
|
|
405
|
+
class HTTP3ConnectionCore:
|
|
406
|
+
state: HTTP3ConnectionState = field(default_factory=HTTP3ConnectionState)
|
|
407
|
+
role: str | None = None
|
|
408
|
+
requests: dict[int, HTTP3RequestStream] = field(default_factory=dict)
|
|
409
|
+
qpack_encoder: QpackEncoder = field(default_factory=QpackEncoder)
|
|
410
|
+
qpack_decoder: QpackDecoder = field(default_factory=QpackDecoder)
|
|
411
|
+
max_request_parse_buffer_size: int = DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT
|
|
412
|
+
|
|
413
|
+
def _update_request_codecs(self) -> None:
|
|
414
|
+
for request in self.requests.values():
|
|
415
|
+
request.qpack_encoder = self.qpack_encoder
|
|
416
|
+
request.qpack_decoder = self.qpack_decoder
|
|
417
|
+
request.connection_state = self.state
|
|
418
|
+
request.role = self.role
|
|
419
|
+
|
|
420
|
+
def _configure_local_decoder(self) -> None:
|
|
421
|
+
capacity = self.state.local_settings.get(SETTING_QPACK_MAX_TABLE_CAPACITY, 0)
|
|
422
|
+
blocked = self.state.local_settings.get(SETTING_QPACK_BLOCKED_STREAMS, 0)
|
|
423
|
+
self.qpack_decoder = QpackDecoder(max_table_capacity=capacity, blocked_streams=blocked)
|
|
424
|
+
self._update_request_codecs()
|
|
425
|
+
|
|
426
|
+
def _configure_remote_encoder(self) -> None:
|
|
427
|
+
capacity = self.state.remote_settings.get(SETTING_QPACK_MAX_TABLE_CAPACITY, 0)
|
|
428
|
+
blocked = self.state.remote_settings.get(SETTING_QPACK_BLOCKED_STREAMS, 0)
|
|
429
|
+
self.qpack_encoder = QpackEncoder(max_table_capacity=capacity, blocked_streams=blocked)
|
|
430
|
+
self._update_request_codecs()
|
|
431
|
+
|
|
432
|
+
def encode_control_stream(self, settings: dict[int, int]) -> bytes:
|
|
433
|
+
if self.state.control_stream_opened:
|
|
434
|
+
raise ProtocolError('HTTP/3 endpoints must not open multiple local control streams')
|
|
435
|
+
self.state.local_settings.update(settings)
|
|
436
|
+
self.state.control_stream_opened = True
|
|
437
|
+
self._configure_local_decoder()
|
|
438
|
+
return encode_quic_varint(STREAM_TYPE_CONTROL) + encode_frame(FRAME_SETTINGS, encode_settings(settings))
|
|
439
|
+
|
|
440
|
+
def encode_goaway(self, identifier: int) -> bytes:
|
|
441
|
+
if self.state.local_goaway_id is not None and identifier > self.state.local_goaway_id:
|
|
442
|
+
raise ProtocolError('HTTP/3 GOAWAY identifier must not increase')
|
|
443
|
+
self.state.local_goaway_id = identifier
|
|
444
|
+
self.state.goaway_stream_id = identifier
|
|
445
|
+
return encode_frame(FRAME_GOAWAY, encode_quic_varint(identifier))
|
|
446
|
+
|
|
447
|
+
def encode_cancel_push(self, push_id: int) -> bytes:
|
|
448
|
+
return encode_frame(FRAME_CANCEL_PUSH, encode_quic_varint(push_id))
|
|
449
|
+
|
|
450
|
+
def encode_max_push_id(self, push_id: int) -> bytes:
|
|
451
|
+
if self.state.local_max_push_id is not None and push_id < self.state.local_max_push_id:
|
|
452
|
+
raise ProtocolError('HTTP/3 MAX_PUSH_ID must not decrease')
|
|
453
|
+
self.state.local_max_push_id = push_id
|
|
454
|
+
return encode_frame(FRAME_MAX_PUSH_ID, encode_quic_varint(push_id))
|
|
455
|
+
|
|
456
|
+
def encode_push_promise(self, stream_id: int, push_id: int, headers: list[tuple[bytes, bytes]]) -> bytes:
|
|
457
|
+
if self.qpack_encoder is not None:
|
|
458
|
+
header_block = self.qpack_encoder.encode_field_section(headers, stream_id=stream_id)
|
|
459
|
+
else:
|
|
460
|
+
header_block = encode_field_section(headers)
|
|
461
|
+
payload = encode_quic_varint(push_id) + header_block
|
|
462
|
+
return encode_frame(FRAME_PUSH_PROMISE, payload)
|
|
463
|
+
|
|
464
|
+
def get_request(self, stream_id: int) -> HTTP3RequestStream:
|
|
465
|
+
return self.requests.setdefault(
|
|
466
|
+
stream_id,
|
|
467
|
+
HTTP3RequestStream(
|
|
468
|
+
state=HTTP3RequestState(stream_id=stream_id),
|
|
469
|
+
qpack_encoder=self.qpack_encoder,
|
|
470
|
+
qpack_decoder=self.qpack_decoder,
|
|
471
|
+
connection_state=self.state,
|
|
472
|
+
role=self.role,
|
|
473
|
+
parse_buffer_limit=self.max_request_parse_buffer_size,
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def abandon_stream(self, stream_id: int) -> None:
|
|
478
|
+
request = self.requests.get(stream_id)
|
|
479
|
+
if request is None:
|
|
480
|
+
return
|
|
481
|
+
request.abandon()
|
|
482
|
+
|
|
483
|
+
def encode_headers(self, stream_id: int, headers: list[tuple[bytes, bytes]]) -> bytes:
|
|
484
|
+
return self.qpack_encoder.encode_field_section(headers, stream_id=stream_id)
|
|
485
|
+
|
|
486
|
+
def take_encoder_stream_data(self) -> bytes:
|
|
487
|
+
return self.qpack_encoder.take_encoder_stream_data()
|
|
488
|
+
|
|
489
|
+
def take_decoder_stream_data(self) -> bytes:
|
|
490
|
+
return self.qpack_decoder.take_decoder_stream_data()
|
|
491
|
+
|
|
492
|
+
def ready_request_states(self) -> list[HTTP3RequestState]:
|
|
493
|
+
return [request.state for request in self.requests.values() if request.state.ready]
|
|
494
|
+
|
|
495
|
+
def _retry_blocked_requests(self) -> None:
|
|
496
|
+
for request in self.requests.values():
|
|
497
|
+
request.retry_blocked()
|
|
498
|
+
|
|
499
|
+
def _process_goaway(self, identifier: int, *, sender_is_client: bool) -> None:
|
|
500
|
+
direction = 'client' if sender_is_client else 'server'
|
|
501
|
+
previous = self.state.peer_goaway_id
|
|
502
|
+
if previous is not None and identifier > previous:
|
|
503
|
+
raise HTTP3ConnectionError('GOAWAY identifier increased', error_code=H3_ID_ERROR)
|
|
504
|
+
if not sender_is_client and (identifier & 0x03) != 0:
|
|
505
|
+
raise HTTP3ConnectionError('server GOAWAY identifier must be a client-initiated bidirectional stream ID', error_code=H3_ID_ERROR)
|
|
506
|
+
self.state.peer_goaway_direction = direction
|
|
507
|
+
self.state.peer_goaway_id = identifier
|
|
508
|
+
self.state.goaway_stream_id = identifier
|
|
509
|
+
|
|
510
|
+
def _process_cancel_push(self, push_id: int, *, sender_is_client: bool) -> None:
|
|
511
|
+
if sender_is_client:
|
|
512
|
+
max_push_id = self.state.peer_max_push_id
|
|
513
|
+
if max_push_id is not None and push_id > max_push_id:
|
|
514
|
+
raise HTTP3ConnectionError('CANCEL_PUSH exceeds peer MAX_PUSH_ID', error_code=H3_ID_ERROR)
|
|
515
|
+
if push_id not in self.state.promised_pushes:
|
|
516
|
+
raise HTTP3ConnectionError('CANCEL_PUSH references unknown promised push', error_code=H3_ID_ERROR)
|
|
517
|
+
else:
|
|
518
|
+
local_max_push_id = self.state.local_max_push_id
|
|
519
|
+
if local_max_push_id is not None and push_id > local_max_push_id:
|
|
520
|
+
raise HTTP3ConnectionError('CANCEL_PUSH exceeds advertised MAX_PUSH_ID', error_code=H3_ID_ERROR)
|
|
521
|
+
self.state.cancelled_push_ids.add(push_id)
|
|
522
|
+
|
|
523
|
+
def _process_max_push_id(self, push_id: int, *, sender_is_client: bool) -> None:
|
|
524
|
+
if not sender_is_client:
|
|
525
|
+
raise HTTP3ConnectionError('server sent MAX_PUSH_ID', error_code=H3_FRAME_UNEXPECTED)
|
|
526
|
+
if self.state.peer_max_push_id is not None and push_id < self.state.peer_max_push_id:
|
|
527
|
+
raise HTTP3ConnectionError('MAX_PUSH_ID must not decrease', error_code=H3_ID_ERROR)
|
|
528
|
+
self.state.peer_max_push_id = push_id
|
|
529
|
+
|
|
530
|
+
def _receive_control_frame(self, frame_type: int, payload: bytes, *, stream_id: int) -> None:
|
|
531
|
+
sender_is_client = _control_sender_is_client(stream_id)
|
|
532
|
+
if frame_type == FRAME_SETTINGS:
|
|
533
|
+
raise HTTP3ConnectionError('duplicate HTTP/3 SETTINGS frame', error_code=H3_FRAME_UNEXPECTED)
|
|
534
|
+
if frame_type == FRAME_GOAWAY:
|
|
535
|
+
identifier = decode_single_varint(payload, context='GOAWAY')
|
|
536
|
+
self._process_goaway(identifier, sender_is_client=sender_is_client)
|
|
537
|
+
return
|
|
538
|
+
if frame_type == FRAME_CANCEL_PUSH:
|
|
539
|
+
push_id = decode_single_varint(payload, context='CANCEL_PUSH')
|
|
540
|
+
self._process_cancel_push(push_id, sender_is_client=sender_is_client)
|
|
541
|
+
return
|
|
542
|
+
if frame_type == FRAME_MAX_PUSH_ID:
|
|
543
|
+
push_id = decode_single_varint(payload, context='MAX_PUSH_ID')
|
|
544
|
+
self._process_max_push_id(push_id, sender_is_client=sender_is_client)
|
|
545
|
+
return
|
|
546
|
+
if frame_type in {FRAME_PUSH_PROMISE, FRAME_HEADERS, FRAME_DATA}:
|
|
547
|
+
raise HTTP3ConnectionError('frame not permitted on control stream', error_code=H3_FRAME_UNEXPECTED)
|
|
548
|
+
# Unknown or reserved frame types are ignored after SETTINGS.
|
|
549
|
+
|
|
550
|
+
def _receive_uni_stream(self, stream_id: int, payload: bytes, *, fin: bool = False) -> None:
|
|
551
|
+
state = self.state.uni_streams.setdefault(stream_id, HTTP3UniStreamState(stream_id=stream_id))
|
|
552
|
+
state.parse_buffer.extend(payload)
|
|
553
|
+
offset = 0
|
|
554
|
+
data = bytes(state.parse_buffer)
|
|
555
|
+
if state.stream_type is None:
|
|
556
|
+
try:
|
|
557
|
+
state.stream_type, offset = decode_quic_varint(data, offset)
|
|
558
|
+
except ProtocolError:
|
|
559
|
+
if fin:
|
|
560
|
+
state.parse_buffer.clear()
|
|
561
|
+
return
|
|
562
|
+
if state.stream_type == STREAM_TYPE_CONTROL:
|
|
563
|
+
if self.state.remote_control_stream_id is None:
|
|
564
|
+
self.state.remote_control_stream_id = stream_id
|
|
565
|
+
elif self.state.remote_control_stream_id != stream_id:
|
|
566
|
+
raise HTTP3ConnectionError('peer opened more than one control stream', error_code=H3_STREAM_CREATION_ERROR)
|
|
567
|
+
elif state.stream_type == STREAM_TYPE_QPACK_ENCODER:
|
|
568
|
+
if self.state.remote_qpack_encoder_stream_id is None:
|
|
569
|
+
self.state.remote_qpack_encoder_stream_id = stream_id
|
|
570
|
+
elif self.state.remote_qpack_encoder_stream_id != stream_id:
|
|
571
|
+
raise HTTP3ConnectionError('peer opened more than one QPACK encoder stream', error_code=H3_STREAM_CREATION_ERROR)
|
|
572
|
+
elif state.stream_type == STREAM_TYPE_QPACK_DECODER:
|
|
573
|
+
if self.state.remote_qpack_decoder_stream_id is None:
|
|
574
|
+
self.state.remote_qpack_decoder_stream_id = stream_id
|
|
575
|
+
elif self.state.remote_qpack_decoder_stream_id != stream_id:
|
|
576
|
+
raise HTTP3ConnectionError('peer opened more than one QPACK decoder stream', error_code=H3_STREAM_CREATION_ERROR)
|
|
577
|
+
elif state.stream_type == STREAM_TYPE_PUSH:
|
|
578
|
+
if _control_sender_is_client(stream_id):
|
|
579
|
+
raise HTTP3ConnectionError('client-initiated push stream is forbidden', error_code=H3_STREAM_CREATION_ERROR)
|
|
580
|
+
self.state.remote_push_stream_ids.add(stream_id)
|
|
581
|
+
else:
|
|
582
|
+
state.discard_stream = True
|
|
583
|
+
if state.stream_type == STREAM_TYPE_CONTROL:
|
|
584
|
+
if fin:
|
|
585
|
+
raise HTTP3ConnectionError('HTTP/3 control stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
|
|
586
|
+
while offset < len(data):
|
|
587
|
+
try:
|
|
588
|
+
frame, next_offset = decode_frame(data, offset)
|
|
589
|
+
except ProtocolError:
|
|
590
|
+
break
|
|
591
|
+
offset = next_offset
|
|
592
|
+
if not state.settings_received:
|
|
593
|
+
if frame.frame_type != FRAME_SETTINGS:
|
|
594
|
+
raise HTTP3ConnectionError('control stream must begin with SETTINGS', error_code=H3_MISSING_SETTINGS)
|
|
595
|
+
state.settings_received = True
|
|
596
|
+
try:
|
|
597
|
+
self.state.remote_settings.update(decode_settings(frame.payload))
|
|
598
|
+
except HTTP3ConnectionError:
|
|
599
|
+
raise
|
|
600
|
+
except ProtocolError as exc:
|
|
601
|
+
raise HTTP3ConnectionError('invalid HTTP/3 SETTINGS payload', error_code=H3_SETTINGS_ERROR) from exc
|
|
602
|
+
self._configure_remote_encoder()
|
|
603
|
+
continue
|
|
604
|
+
self._receive_control_frame(frame.frame_type, frame.payload, stream_id=stream_id)
|
|
605
|
+
remaining = data[offset:]
|
|
606
|
+
state.parse_buffer.clear()
|
|
607
|
+
state.parse_buffer.extend(remaining)
|
|
608
|
+
return
|
|
609
|
+
if state.stream_type == STREAM_TYPE_QPACK_ENCODER:
|
|
610
|
+
if fin:
|
|
611
|
+
raise HTTP3ConnectionError('HTTP/3 QPACK encoder stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
|
|
612
|
+
try:
|
|
613
|
+
self.qpack_decoder.receive_encoder_stream(data[offset:])
|
|
614
|
+
except QpackEncoderStreamError as exc:
|
|
615
|
+
raise HTTP3ConnectionError('invalid QPACK encoder stream data', error_code=QPACK_ENCODER_STREAM_ERROR) from exc
|
|
616
|
+
except ProtocolError as exc:
|
|
617
|
+
raise HTTP3ConnectionError('invalid QPACK encoder stream data', error_code=QPACK_ENCODER_STREAM_ERROR) from exc
|
|
618
|
+
finally:
|
|
619
|
+
state.parse_buffer.clear()
|
|
620
|
+
self._retry_blocked_requests()
|
|
621
|
+
return
|
|
622
|
+
if state.stream_type == STREAM_TYPE_QPACK_DECODER:
|
|
623
|
+
if fin:
|
|
624
|
+
raise HTTP3ConnectionError('HTTP/3 QPACK decoder stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
|
|
625
|
+
try:
|
|
626
|
+
self.qpack_encoder.receive_decoder_stream(data[offset:])
|
|
627
|
+
except QpackDecoderStreamError as exc:
|
|
628
|
+
raise HTTP3ConnectionError('invalid QPACK decoder stream data', error_code=QPACK_DECODER_STREAM_ERROR) from exc
|
|
629
|
+
except ProtocolError as exc:
|
|
630
|
+
raise HTTP3ConnectionError('invalid QPACK decoder stream data', error_code=QPACK_DECODER_STREAM_ERROR) from exc
|
|
631
|
+
finally:
|
|
632
|
+
state.parse_buffer.clear()
|
|
633
|
+
return
|
|
634
|
+
if state.stream_type == STREAM_TYPE_PUSH:
|
|
635
|
+
if state.push_id is None:
|
|
636
|
+
try:
|
|
637
|
+
state.push_id, offset = decode_quic_varint(data, offset)
|
|
638
|
+
except ProtocolError:
|
|
639
|
+
if fin:
|
|
640
|
+
state.parse_buffer.clear()
|
|
641
|
+
return
|
|
642
|
+
for other in self.state.uni_streams.values():
|
|
643
|
+
if other is state or other.stream_type != STREAM_TYPE_PUSH or other.push_id is None:
|
|
644
|
+
continue
|
|
645
|
+
if other.push_id == state.push_id:
|
|
646
|
+
raise HTTP3ConnectionError('push stream reused push ID', error_code=H3_ID_ERROR)
|
|
647
|
+
state.parse_buffer.clear()
|
|
648
|
+
return
|
|
649
|
+
state.parse_buffer.clear()
|
|
650
|
+
|
|
651
|
+
def receive_stream_data(self, stream_id: int, payload: bytes, *, fin: bool = False) -> HTTP3RequestState | None:
|
|
652
|
+
if stream_id & 0x02:
|
|
653
|
+
self._receive_uni_stream(stream_id, payload, fin=fin)
|
|
654
|
+
return None
|
|
655
|
+
if self.role == 'server' and self.state.local_goaway_id is not None and stream_id >= self.state.local_goaway_id and stream_id not in self.requests:
|
|
656
|
+
raise HTTP3StreamError('request rejected after GOAWAY', error_code=H3_REQUEST_REJECTED, stream_id=stream_id)
|
|
657
|
+
return self.get_request(stream_id).receive(payload, fin=fin)
|