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,360 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from typing import Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from tigrcorn_protocols.flow.keepalive import KeepAlivePolicy, KeepAliveRuntime
|
|
8
|
+
from tigrcorn_observability.metrics import Metrics
|
|
9
|
+
|
|
10
|
+
from tigrcorn_asgi.events.websocket import websocket_connect, websocket_disconnect, websocket_receive_bytes, websocket_receive_text
|
|
11
|
+
from tigrcorn_asgi.receive import QueueReceive
|
|
12
|
+
from tigrcorn_asgi.scopes.websocket import build_websocket_scope
|
|
13
|
+
from tigrcorn_config.model import ServerConfig
|
|
14
|
+
from tigrcorn_core.errors import ProtocolError
|
|
15
|
+
from tigrcorn_protocols.http1.parser import ParsedRequest
|
|
16
|
+
from tigrcorn_protocols.websocket.codec import binary_frame, close_frame, pong_frame, text_frame
|
|
17
|
+
from tigrcorn_protocols.websocket.frames import OP_BINARY, OP_CLOSE, OP_CONT, OP_PING, OP_PONG, OP_TEXT, decode_close_payload, parse_frame_bytes, serialize_frame
|
|
18
|
+
from tigrcorn_protocols.websocket.extensions import PerMessageDeflateRuntime, default_permessage_deflate_agreement, negotiate_permessage_deflate, parse_permessage_deflate_offers
|
|
19
|
+
from tigrcorn_core.types import ASGIApp
|
|
20
|
+
from tigrcorn_core.utils.headers import get_header
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class H2WebSocketSession:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
app: ASGIApp,
|
|
28
|
+
config: ServerConfig,
|
|
29
|
+
request: ParsedRequest,
|
|
30
|
+
client: tuple[str, int] | None,
|
|
31
|
+
server: tuple[str, int] | tuple[str, None] | None,
|
|
32
|
+
scheme: str,
|
|
33
|
+
send_headers: Callable[[int, list[tuple[bytes, bytes]], bool], Awaitable[None]],
|
|
34
|
+
send_data: Callable[[bytes, bool], Awaitable[None]],
|
|
35
|
+
metrics: Metrics | None = None,
|
|
36
|
+
on_close: Callable[[], None] | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.app = app
|
|
39
|
+
self.config = config
|
|
40
|
+
self.request = request
|
|
41
|
+
self.client = client
|
|
42
|
+
self.server = server
|
|
43
|
+
self.scheme = 'wss' if scheme == 'https' else 'ws'
|
|
44
|
+
self.send_headers = send_headers
|
|
45
|
+
self.send_data = send_data
|
|
46
|
+
self.metrics = metrics
|
|
47
|
+
self.on_close = on_close
|
|
48
|
+
self.receive = QueueReceive(max_size=self.config.websocket.max_queue)
|
|
49
|
+
self.task: asyncio.Task[None] | None = None
|
|
50
|
+
self.accepted = False
|
|
51
|
+
self.closed = False
|
|
52
|
+
self.http_denied = False
|
|
53
|
+
self.http_denial_status = 403
|
|
54
|
+
self.http_denial_headers: list[tuple[bytes, bytes]] = []
|
|
55
|
+
self.http_denial_started = False
|
|
56
|
+
self.subprotocols = build_websocket_scope(request, client=client, server=server, scheme=self.scheme, root_path=self.config.proxy.root_path, proxy=self.config.proxy)['subprotocols']
|
|
57
|
+
self.buffer = bytearray()
|
|
58
|
+
self.peer_end_stream_pending = False
|
|
59
|
+
self.fragmented_opcode: int | None = None
|
|
60
|
+
self.fragments: list[bytes] = []
|
|
61
|
+
self.current_message_size = 0
|
|
62
|
+
self.fragmented_compressed = False
|
|
63
|
+
self.permessage_deflate_offers = parse_permessage_deflate_offers(request.headers)
|
|
64
|
+
self.permessage_deflate_runtime: PerMessageDeflateRuntime | None = None
|
|
65
|
+
self.keepalive_policy = KeepAlivePolicy(
|
|
66
|
+
idle_timeout=self.config.http.idle_timeout,
|
|
67
|
+
ping_interval=self.config.websocket.ping_interval,
|
|
68
|
+
ping_timeout=self.config.websocket.ping_timeout,
|
|
69
|
+
)
|
|
70
|
+
self.keepalive = KeepAliveRuntime(self.keepalive_policy) if self.keepalive_policy.enabled else None
|
|
71
|
+
self.keepalive_task: asyncio.Task[None] | None = None
|
|
72
|
+
version = get_header(request.headers, b'sec-websocket-version')
|
|
73
|
+
if version != b'13':
|
|
74
|
+
raise ProtocolError('unsupported websocket version')
|
|
75
|
+
|
|
76
|
+
async def start(self) -> None:
|
|
77
|
+
scope = build_websocket_scope(self.request, client=self.client, server=self.server, scheme=self.scheme, root_path=self.config.proxy.root_path, proxy=self.config.proxy)
|
|
78
|
+
await self.receive.put(websocket_connect())
|
|
79
|
+
self.task = asyncio.create_task(self._run_app(scope), name=f'tigrcorn-h2-ws-{self.request.path}')
|
|
80
|
+
if self.keepalive is not None:
|
|
81
|
+
self.keepalive_task = asyncio.create_task(self._keepalive_loop(), name=f'tigrcorn-h2-ws-keepalive-{self.request.path}')
|
|
82
|
+
|
|
83
|
+
def _record_activity(self) -> None:
|
|
84
|
+
if self.keepalive is not None:
|
|
85
|
+
self.keepalive.record_activity()
|
|
86
|
+
|
|
87
|
+
def _notify_closed(self) -> None:
|
|
88
|
+
if self.on_close is not None:
|
|
89
|
+
callback = self.on_close
|
|
90
|
+
self.on_close = None
|
|
91
|
+
callback()
|
|
92
|
+
|
|
93
|
+
async def _keepalive_loop(self) -> None:
|
|
94
|
+
while not self.closed:
|
|
95
|
+
await asyncio.sleep(0.05)
|
|
96
|
+
if self.keepalive is None or self.closed:
|
|
97
|
+
return
|
|
98
|
+
if self.keepalive.ping_timed_out():
|
|
99
|
+
if self.metrics is not None:
|
|
100
|
+
self.metrics.websocket_ping_timeout()
|
|
101
|
+
if not self.closed:
|
|
102
|
+
await self.send_data(close_frame(1011, 'ping timeout'), True)
|
|
103
|
+
self.closed = True
|
|
104
|
+
self._notify_closed()
|
|
105
|
+
await self.receive.put(websocket_disconnect(1011, 'ping timeout'))
|
|
106
|
+
return
|
|
107
|
+
payload = self.keepalive.next_ping_payload()
|
|
108
|
+
if payload is None:
|
|
109
|
+
continue
|
|
110
|
+
if self.metrics is not None:
|
|
111
|
+
self.metrics.websocket_ping_sent()
|
|
112
|
+
await self.send_data(serialize_frame(OP_PING, payload), False)
|
|
113
|
+
|
|
114
|
+
async def _run_app(self, scope: dict) -> None:
|
|
115
|
+
try:
|
|
116
|
+
await self.app(scope, self.receive, self._send)
|
|
117
|
+
except Exception:
|
|
118
|
+
if self.accepted and not self.closed:
|
|
119
|
+
with suppress(Exception):
|
|
120
|
+
await self.send_data(close_frame(1011, 'internal error'), True)
|
|
121
|
+
raise
|
|
122
|
+
finally:
|
|
123
|
+
if self.http_denied and not self.closed:
|
|
124
|
+
if not self.http_denial_started:
|
|
125
|
+
await self.send_headers(self.http_denial_status, self.http_denial_headers, True)
|
|
126
|
+
self.http_denial_started = True
|
|
127
|
+
self.closed = True
|
|
128
|
+
elif not self.accepted and not self.closed:
|
|
129
|
+
await self.send_headers(403, [], True)
|
|
130
|
+
self.closed = True
|
|
131
|
+
elif self.accepted and not self.closed:
|
|
132
|
+
await self.send_data(close_frame(1000, ''), True)
|
|
133
|
+
self.closed = True
|
|
134
|
+
self._notify_closed()
|
|
135
|
+
if self.keepalive_task is not None:
|
|
136
|
+
self.keepalive_task.cancel()
|
|
137
|
+
with suppress(asyncio.CancelledError):
|
|
138
|
+
await self.keepalive_task
|
|
139
|
+
|
|
140
|
+
async def _send(self, message: dict) -> None:
|
|
141
|
+
typ = message['type']
|
|
142
|
+
if typ == 'websocket.accept':
|
|
143
|
+
if self.accepted or self.http_denied:
|
|
144
|
+
raise RuntimeError('websocket.accept sent more than once')
|
|
145
|
+
subprotocol = message.get('subprotocol')
|
|
146
|
+
if subprotocol is not None and subprotocol not in self.subprotocols:
|
|
147
|
+
raise RuntimeError('websocket.accept selected a subprotocol not offered by the client')
|
|
148
|
+
headers = [(bytes(k).lower(), bytes(v)) for k, v in message.get('headers', [])]
|
|
149
|
+
if self.config.websocket.compression != 'permessage-deflate':
|
|
150
|
+
headers = [(k, v) for k, v in headers if k != b'sec-websocket-extensions']
|
|
151
|
+
elif self.permessage_deflate_offers and get_header(headers, b'sec-websocket-extensions') is None:
|
|
152
|
+
default_agreement = default_permessage_deflate_agreement(self.permessage_deflate_offers)
|
|
153
|
+
if default_agreement is not None:
|
|
154
|
+
headers = headers + [(b'sec-websocket-extensions', default_agreement.as_header_value())]
|
|
155
|
+
response_headers = [(k, v) for k, v in headers if k not in {b'sec-websocket-extensions', b'sec-websocket-protocol'}]
|
|
156
|
+
agreement = negotiate_permessage_deflate(
|
|
157
|
+
request_headers=self.request.headers,
|
|
158
|
+
response_headers=headers,
|
|
159
|
+
)
|
|
160
|
+
if agreement is not None:
|
|
161
|
+
self.permessage_deflate_runtime = PerMessageDeflateRuntime(agreement)
|
|
162
|
+
response_headers.append((b'sec-websocket-extensions', agreement.as_header_value()))
|
|
163
|
+
if subprotocol is not None:
|
|
164
|
+
response_headers.append((b'sec-websocket-protocol', subprotocol.encode('ascii')))
|
|
165
|
+
await self.send_headers(200, response_headers, False)
|
|
166
|
+
self.accepted = True
|
|
167
|
+
self._record_activity()
|
|
168
|
+
if self.buffer or self.peer_end_stream_pending:
|
|
169
|
+
pending_end_stream = self.peer_end_stream_pending
|
|
170
|
+
self.peer_end_stream_pending = False
|
|
171
|
+
await self.feed_data(b'', end_stream=pending_end_stream)
|
|
172
|
+
return
|
|
173
|
+
if typ == 'websocket.send':
|
|
174
|
+
if not self.accepted:
|
|
175
|
+
raise RuntimeError('websocket.send before websocket.accept')
|
|
176
|
+
if self.closed:
|
|
177
|
+
return
|
|
178
|
+
text = message.get('text')
|
|
179
|
+
data = message.get('bytes')
|
|
180
|
+
if text is not None and data is not None:
|
|
181
|
+
raise RuntimeError('websocket.send cannot contain both text and bytes')
|
|
182
|
+
if text is not None:
|
|
183
|
+
payload = text.encode('utf-8')
|
|
184
|
+
if self.permessage_deflate_runtime is not None:
|
|
185
|
+
await self.send_data(serialize_frame(OP_TEXT, self.permessage_deflate_runtime.compress_message(payload), rsv1=True), False)
|
|
186
|
+
else:
|
|
187
|
+
await self.send_data(text_frame(text), False)
|
|
188
|
+
self._record_activity()
|
|
189
|
+
else:
|
|
190
|
+
raw = data or b''
|
|
191
|
+
if self.permessage_deflate_runtime is not None:
|
|
192
|
+
await self.send_data(binary_frame(self.permessage_deflate_runtime.compress_message(raw), rsv1=True), False)
|
|
193
|
+
else:
|
|
194
|
+
await self.send_data(binary_frame(raw), False)
|
|
195
|
+
self._record_activity()
|
|
196
|
+
return
|
|
197
|
+
if typ == 'websocket.close':
|
|
198
|
+
code = int(message.get('code', 1000))
|
|
199
|
+
reason = message.get('reason', '')
|
|
200
|
+
if not self.accepted:
|
|
201
|
+
self.http_denied = True
|
|
202
|
+
self.http_denial_status = 403
|
|
203
|
+
self.http_denial_headers = []
|
|
204
|
+
return
|
|
205
|
+
if not self.closed:
|
|
206
|
+
await self.send_data(close_frame(code, reason), True)
|
|
207
|
+
self.closed = True
|
|
208
|
+
self._notify_closed()
|
|
209
|
+
return
|
|
210
|
+
if typ == 'websocket.http.response.start':
|
|
211
|
+
if self.accepted:
|
|
212
|
+
raise RuntimeError('cannot deny websocket after accept')
|
|
213
|
+
self.http_denied = True
|
|
214
|
+
self.http_denial_status = int(message['status'])
|
|
215
|
+
self.http_denial_headers = list(message.get('headers', []))
|
|
216
|
+
return
|
|
217
|
+
if typ == 'websocket.http.response.body':
|
|
218
|
+
if not self.http_denied:
|
|
219
|
+
raise RuntimeError('websocket.http.response.body before denial start')
|
|
220
|
+
body = bytes(message.get('body', b''))
|
|
221
|
+
more = bool(message.get('more_body', False))
|
|
222
|
+
if not self.http_denial_started:
|
|
223
|
+
headers = list(self.http_denial_headers)
|
|
224
|
+
if not more:
|
|
225
|
+
headers.append((b'content-length', str(len(body)).encode('ascii')))
|
|
226
|
+
end_stream = (not body) and (not more)
|
|
227
|
+
await self.send_headers(self.http_denial_status, headers, end_stream)
|
|
228
|
+
self.http_denial_started = True
|
|
229
|
+
if end_stream:
|
|
230
|
+
self.closed = True
|
|
231
|
+
return
|
|
232
|
+
if body or not more:
|
|
233
|
+
await self.send_data(body, not more)
|
|
234
|
+
if not more:
|
|
235
|
+
self.closed = True
|
|
236
|
+
return
|
|
237
|
+
raise RuntimeError(f'unexpected websocket send message: {typ!r}')
|
|
238
|
+
|
|
239
|
+
def _frame_length(self, data: bytes) -> int | None:
|
|
240
|
+
if len(data) < 2:
|
|
241
|
+
return None
|
|
242
|
+
masked = bool(data[1] & 0x80)
|
|
243
|
+
length = data[1] & 0x7F
|
|
244
|
+
pos = 2
|
|
245
|
+
if length == 126:
|
|
246
|
+
if len(data) < pos + 2:
|
|
247
|
+
return None
|
|
248
|
+
length = int.from_bytes(data[pos:pos + 2], 'big')
|
|
249
|
+
pos += 2
|
|
250
|
+
elif length == 127:
|
|
251
|
+
if len(data) < pos + 8:
|
|
252
|
+
return None
|
|
253
|
+
length = int.from_bytes(data[pos:pos + 8], 'big')
|
|
254
|
+
pos += 8
|
|
255
|
+
if masked:
|
|
256
|
+
pos += 4
|
|
257
|
+
if len(data) < pos + length:
|
|
258
|
+
return None
|
|
259
|
+
return pos + length
|
|
260
|
+
|
|
261
|
+
def _inflate_if_needed(self, frame_payload: bytes, rsv1: bool) -> bytes:
|
|
262
|
+
if not rsv1:
|
|
263
|
+
return frame_payload
|
|
264
|
+
if self.permessage_deflate_runtime is None:
|
|
265
|
+
raise ProtocolError('RSV1 is not negotiated')
|
|
266
|
+
return self.permessage_deflate_runtime.decompress_message(frame_payload)
|
|
267
|
+
|
|
268
|
+
async def feed_data(self, data: bytes, *, end_stream: bool = False) -> None:
|
|
269
|
+
if self.closed:
|
|
270
|
+
return
|
|
271
|
+
self.buffer.extend(data)
|
|
272
|
+
if end_stream:
|
|
273
|
+
self.peer_end_stream_pending = True
|
|
274
|
+
if not self.accepted and not self.http_denied:
|
|
275
|
+
return
|
|
276
|
+
while self.buffer:
|
|
277
|
+
frame_len = self._frame_length(self.buffer)
|
|
278
|
+
if frame_len is None:
|
|
279
|
+
break
|
|
280
|
+
raw = bytes(self.buffer[:frame_len])
|
|
281
|
+
del self.buffer[:frame_len]
|
|
282
|
+
frame = parse_frame_bytes(
|
|
283
|
+
raw,
|
|
284
|
+
expect_masked=True,
|
|
285
|
+
max_payload_size=self.config.websocket_max_message_size,
|
|
286
|
+
allow_rsv1=self.permessage_deflate_runtime is not None,
|
|
287
|
+
)
|
|
288
|
+
self._record_activity()
|
|
289
|
+
if frame.opcode == OP_PING:
|
|
290
|
+
await self.send_data(pong_frame(frame.payload), False)
|
|
291
|
+
continue
|
|
292
|
+
if frame.opcode == OP_PONG:
|
|
293
|
+
if self.keepalive is not None:
|
|
294
|
+
self.keepalive.acknowledge_pong(frame.payload)
|
|
295
|
+
continue
|
|
296
|
+
if frame.opcode == OP_CLOSE:
|
|
297
|
+
code, reason = decode_close_payload(frame.payload)
|
|
298
|
+
if not self.closed:
|
|
299
|
+
await self.send_data(close_frame(code, reason), True)
|
|
300
|
+
self.closed = True
|
|
301
|
+
self._notify_closed()
|
|
302
|
+
await self.receive.put(websocket_disconnect(code, reason))
|
|
303
|
+
break
|
|
304
|
+
if frame.opcode in {OP_TEXT, OP_BINARY}:
|
|
305
|
+
if self.fragmented_opcode is not None:
|
|
306
|
+
raise ProtocolError('new data frame before fragmented message completion')
|
|
307
|
+
self.current_message_size = len(frame.payload)
|
|
308
|
+
if self.current_message_size > self.config.websocket_max_message_size:
|
|
309
|
+
raise ProtocolError('message too big')
|
|
310
|
+
if frame.fin:
|
|
311
|
+
payload = self._inflate_if_needed(frame.payload, frame.rsv1)
|
|
312
|
+
if frame.opcode == OP_TEXT:
|
|
313
|
+
await self.receive.put(websocket_receive_text(payload.decode('utf-8')))
|
|
314
|
+
else:
|
|
315
|
+
await self.receive.put(websocket_receive_bytes(payload))
|
|
316
|
+
self.current_message_size = 0
|
|
317
|
+
else:
|
|
318
|
+
self.fragmented_opcode = frame.opcode
|
|
319
|
+
self.fragmented_compressed = frame.rsv1
|
|
320
|
+
self.fragments = [frame.payload]
|
|
321
|
+
continue
|
|
322
|
+
if frame.opcode == OP_CONT:
|
|
323
|
+
if self.fragmented_opcode is None:
|
|
324
|
+
raise ProtocolError('unexpected continuation frame')
|
|
325
|
+
if frame.rsv1:
|
|
326
|
+
raise ProtocolError('RSV1 is only valid on the first frame of a compressed message')
|
|
327
|
+
self.current_message_size += len(frame.payload)
|
|
328
|
+
if self.current_message_size > self.config.websocket_max_message_size:
|
|
329
|
+
raise ProtocolError('message too big')
|
|
330
|
+
self.fragments.append(frame.payload)
|
|
331
|
+
if frame.fin:
|
|
332
|
+
message = b''.join(self.fragments)
|
|
333
|
+
if self.fragmented_compressed:
|
|
334
|
+
message = self._inflate_if_needed(message, True)
|
|
335
|
+
opcode = self.fragmented_opcode
|
|
336
|
+
self.fragmented_opcode = None
|
|
337
|
+
self.fragmented_compressed = False
|
|
338
|
+
self.fragments = []
|
|
339
|
+
self.current_message_size = 0
|
|
340
|
+
if opcode == OP_TEXT:
|
|
341
|
+
await self.receive.put(websocket_receive_text(message.decode('utf-8')))
|
|
342
|
+
else:
|
|
343
|
+
await self.receive.put(websocket_receive_bytes(message))
|
|
344
|
+
continue
|
|
345
|
+
raise ProtocolError('unsupported websocket opcode')
|
|
346
|
+
if self.peer_end_stream_pending and not self.closed:
|
|
347
|
+
self.peer_end_stream_pending = False
|
|
348
|
+
self.closed = True
|
|
349
|
+
self._notify_closed()
|
|
350
|
+
await self.receive.put(websocket_disconnect(1000, ''))
|
|
351
|
+
|
|
352
|
+
async def abort(self) -> None:
|
|
353
|
+
if not self.closed:
|
|
354
|
+
self.closed = True
|
|
355
|
+
self._notify_closed()
|
|
356
|
+
await self.receive.put(websocket_disconnect(1006, ''))
|
|
357
|
+
if self.task is not None:
|
|
358
|
+
self.task.cancel()
|
|
359
|
+
with suppress(asyncio.CancelledError):
|
|
360
|
+
await self.task
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from .codec import (
|
|
2
|
+
H3_FRAME_UNEXPECTED,
|
|
3
|
+
H3_ID_ERROR,
|
|
4
|
+
H3_MESSAGE_ERROR,
|
|
5
|
+
H3_MISSING_SETTINGS,
|
|
6
|
+
H3_REQUEST_INCOMPLETE,
|
|
7
|
+
H3_SETTINGS_ERROR,
|
|
8
|
+
HTTP3ConnectionError,
|
|
9
|
+
HTTP3Frame,
|
|
10
|
+
HTTP3StreamError,
|
|
11
|
+
QPACK_DECODER_STREAM_ERROR,
|
|
12
|
+
QPACK_DECOMPRESSION_FAILED,
|
|
13
|
+
QPACK_ENCODER_STREAM_ERROR,
|
|
14
|
+
decode_frame,
|
|
15
|
+
decode_settings,
|
|
16
|
+
encode_frame,
|
|
17
|
+
encode_settings,
|
|
18
|
+
parse_frames,
|
|
19
|
+
)
|
|
20
|
+
from .qpack import (
|
|
21
|
+
FieldLine,
|
|
22
|
+
QpackBlocked,
|
|
23
|
+
QpackDecoder,
|
|
24
|
+
QpackDecoderStreamError,
|
|
25
|
+
QpackDecompressionFailed,
|
|
26
|
+
QpackEncoder,
|
|
27
|
+
QpackEncoderStreamError,
|
|
28
|
+
decode_field_section,
|
|
29
|
+
encode_field_section,
|
|
30
|
+
)
|
|
31
|
+
from .state import HTTP3BlockedSection, HTTP3ConnectionState, HTTP3PushPromiseState, HTTP3RequestState, HTTP3UniStreamState
|
|
32
|
+
from .streams import HTTP3ConnectionCore, HTTP3RequestStream
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
'HTTP3Frame',
|
|
36
|
+
'FieldLine',
|
|
37
|
+
'QpackBlocked',
|
|
38
|
+
'QpackDecompressionFailed',
|
|
39
|
+
'QpackEncoderStreamError',
|
|
40
|
+
'QpackDecoderStreamError',
|
|
41
|
+
'QpackDecoder',
|
|
42
|
+
'QpackEncoder',
|
|
43
|
+
'HTTP3ConnectionError',
|
|
44
|
+
'HTTP3StreamError',
|
|
45
|
+
'HTTP3BlockedSection',
|
|
46
|
+
'HTTP3PushPromiseState',
|
|
47
|
+
'HTTP3ConnectionState',
|
|
48
|
+
'HTTP3RequestState',
|
|
49
|
+
'HTTP3UniStreamState',
|
|
50
|
+
'HTTP3ConnectionCore',
|
|
51
|
+
'HTTP3RequestStream',
|
|
52
|
+
'HTTP3DatagramHandler',
|
|
53
|
+
'HTTP3Session',
|
|
54
|
+
'encode_frame',
|
|
55
|
+
'decode_frame',
|
|
56
|
+
'parse_frames',
|
|
57
|
+
'encode_settings',
|
|
58
|
+
'decode_settings',
|
|
59
|
+
'encode_field_section',
|
|
60
|
+
'decode_field_section',
|
|
61
|
+
'H3_FRAME_UNEXPECTED',
|
|
62
|
+
'H3_ID_ERROR',
|
|
63
|
+
'H3_MESSAGE_ERROR',
|
|
64
|
+
'H3_MISSING_SETTINGS',
|
|
65
|
+
'H3_REQUEST_INCOMPLETE',
|
|
66
|
+
'H3_SETTINGS_ERROR',
|
|
67
|
+
'QPACK_DECOMPRESSION_FAILED',
|
|
68
|
+
'QPACK_ENCODER_STREAM_ERROR',
|
|
69
|
+
'QPACK_DECODER_STREAM_ERROR',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __getattr__(name: str):
|
|
74
|
+
if name in {"HTTP3DatagramHandler", "HTTP3Session"}:
|
|
75
|
+
from .handler import HTTP3DatagramHandler, HTTP3Session
|
|
76
|
+
|
|
77
|
+
mapping = {
|
|
78
|
+
"HTTP3DatagramHandler": HTTP3DatagramHandler,
|
|
79
|
+
"HTTP3Session": HTTP3Session,
|
|
80
|
+
}
|
|
81
|
+
return mapping[name]
|
|
82
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.errors import ProtocolError
|
|
7
|
+
from tigrcorn_core.utils.bytes import decode_quic_varint, encode_quic_varint
|
|
8
|
+
|
|
9
|
+
FRAME_DATA = 0x0
|
|
10
|
+
FRAME_HEADERS = 0x1
|
|
11
|
+
FRAME_CANCEL_PUSH = 0x3
|
|
12
|
+
FRAME_SETTINGS = 0x4
|
|
13
|
+
FRAME_PUSH_PROMISE = 0x5
|
|
14
|
+
FRAME_GOAWAY = 0x7
|
|
15
|
+
FRAME_MAX_PUSH_ID = 0xD
|
|
16
|
+
STREAM_TYPE_CONTROL = 0x00
|
|
17
|
+
SETTING_ENABLE_CONNECT_PROTOCOL = 0x08
|
|
18
|
+
SETTING_H3_DATAGRAM = 0x33
|
|
19
|
+
SETTING_ENABLE_WEBTRANSPORT = 0x2B603742
|
|
20
|
+
|
|
21
|
+
H3_NO_ERROR = 0x0100
|
|
22
|
+
H3_GENERAL_PROTOCOL_ERROR = 0x0101
|
|
23
|
+
H3_INTERNAL_ERROR = 0x0102
|
|
24
|
+
H3_STREAM_CREATION_ERROR = 0x0103
|
|
25
|
+
H3_CLOSED_CRITICAL_STREAM = 0x0104
|
|
26
|
+
H3_FRAME_UNEXPECTED = 0x0105
|
|
27
|
+
H3_FRAME_ERROR = 0x0106
|
|
28
|
+
H3_EXCESSIVE_LOAD = 0x0107
|
|
29
|
+
H3_ID_ERROR = 0x0108
|
|
30
|
+
H3_SETTINGS_ERROR = 0x0109
|
|
31
|
+
H3_MISSING_SETTINGS = 0x010A
|
|
32
|
+
H3_REQUEST_REJECTED = 0x010B
|
|
33
|
+
H3_REQUEST_CANCELLED = 0x010C
|
|
34
|
+
H3_REQUEST_INCOMPLETE = 0x010D
|
|
35
|
+
H3_MESSAGE_ERROR = 0x010E
|
|
36
|
+
H3_CONNECT_ERROR = 0x010F
|
|
37
|
+
H3_VERSION_FALLBACK = 0x0110
|
|
38
|
+
QPACK_DECOMPRESSION_FAILED = 0x0200
|
|
39
|
+
QPACK_ENCODER_STREAM_ERROR = 0x0201
|
|
40
|
+
QPACK_DECODER_STREAM_ERROR = 0x0202
|
|
41
|
+
|
|
42
|
+
HTTP3_RESERVED_SETTINGS = frozenset({0x00, 0x02, 0x03, 0x04, 0x05})
|
|
43
|
+
HTTP3_RESERVED_FRAME_TYPES = frozenset({0x02, 0x06, 0x08, 0x09})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_reserved_setting(identifier: int) -> bool:
|
|
47
|
+
return identifier in HTTP3_RESERVED_SETTINGS
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_reserved_frame_type(frame_type: int) -> bool:
|
|
52
|
+
return frame_type in HTTP3_RESERVED_FRAME_TYPES
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_grease_identifier(identifier: int) -> bool:
|
|
57
|
+
return identifier >= 0x21 and (identifier - 0x21) % 0x1F == 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HTTP3Error(ProtocolError):
|
|
61
|
+
def __init__(self, message: str, *, error_code: int, stream_id: int | None = None) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.error_code = error_code
|
|
64
|
+
self.stream_id = stream_id
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class HTTP3ConnectionError(HTTP3Error):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class HTTP3StreamError(HTTP3Error):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class HTTP3Frame:
|
|
77
|
+
frame_type: int
|
|
78
|
+
payload: bytes
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def encode_frame(frame_type: int, payload: bytes = b'') -> bytes:
|
|
83
|
+
return encode_quic_varint(frame_type) + encode_quic_varint(len(payload)) + payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def decode_frame(data: bytes, offset: int = 0) -> tuple[HTTP3Frame, int]:
|
|
88
|
+
frame_type, offset = decode_quic_varint(data, offset)
|
|
89
|
+
length, offset = decode_quic_varint(data, offset)
|
|
90
|
+
end = offset + length
|
|
91
|
+
if end > len(data):
|
|
92
|
+
raise ProtocolError('truncated HTTP/3 frame payload')
|
|
93
|
+
return HTTP3Frame(frame_type=frame_type, payload=data[offset:end]), end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_frames(data: bytes) -> list[HTTP3Frame]:
|
|
98
|
+
frames: list[HTTP3Frame] = []
|
|
99
|
+
offset = 0
|
|
100
|
+
while offset < len(data):
|
|
101
|
+
frame, offset = decode_frame(data, offset)
|
|
102
|
+
frames.append(frame)
|
|
103
|
+
return frames
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def encode_settings(settings: Mapping[int, int]) -> bytes:
|
|
108
|
+
payload = bytearray()
|
|
109
|
+
seen: set[int] = set()
|
|
110
|
+
for key, value in settings.items():
|
|
111
|
+
key_int = int(key)
|
|
112
|
+
if key_int in seen:
|
|
113
|
+
raise ProtocolError('duplicate HTTP/3 setting identifier')
|
|
114
|
+
if is_reserved_setting(key_int):
|
|
115
|
+
raise ProtocolError(f'reserved HTTP/3 setting identifier: {key_int:#x}')
|
|
116
|
+
seen.add(key_int)
|
|
117
|
+
payload.extend(encode_quic_varint(key_int))
|
|
118
|
+
payload.extend(encode_quic_varint(int(value)))
|
|
119
|
+
return bytes(payload)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def decode_settings(payload: bytes) -> dict[int, int]:
|
|
124
|
+
settings: dict[int, int] = {}
|
|
125
|
+
offset = 0
|
|
126
|
+
while offset < len(payload):
|
|
127
|
+
try:
|
|
128
|
+
key, offset = decode_quic_varint(payload, offset)
|
|
129
|
+
value, offset = decode_quic_varint(payload, offset)
|
|
130
|
+
except ProtocolError as exc:
|
|
131
|
+
raise HTTP3ConnectionError('malformed HTTP/3 SETTINGS payload', error_code=H3_SETTINGS_ERROR) from exc
|
|
132
|
+
if key in settings:
|
|
133
|
+
raise HTTP3ConnectionError('duplicate HTTP/3 setting', error_code=H3_SETTINGS_ERROR)
|
|
134
|
+
if is_reserved_setting(key):
|
|
135
|
+
raise HTTP3ConnectionError(f'reserved HTTP/3 setting received: {key:#x}', error_code=H3_SETTINGS_ERROR)
|
|
136
|
+
settings[key] = value
|
|
137
|
+
return settings
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def decode_single_varint(payload: bytes, *, context: str) -> int:
|
|
142
|
+
try:
|
|
143
|
+
value, offset = decode_quic_varint(payload, 0)
|
|
144
|
+
except ProtocolError as exc:
|
|
145
|
+
raise HTTP3ConnectionError(f'malformed {context} frame payload', error_code=H3_FRAME_ERROR) from exc
|
|
146
|
+
if offset != len(payload):
|
|
147
|
+
raise HTTP3ConnectionError(f'invalid {context} frame size', error_code=H3_FRAME_ERROR)
|
|
148
|
+
return value
|