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,324 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import zlib
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.errors import ProtocolError
|
|
7
|
+
from tigrcorn_core.utils.headers import get_headers
|
|
8
|
+
|
|
9
|
+
_PERMESSAGE_DEFLATE = b"permessage-deflate"
|
|
10
|
+
_SERVER_NO_CONTEXT_TAKEOVER = b"server_no_context_takeover"
|
|
11
|
+
_CLIENT_NO_CONTEXT_TAKEOVER = b"client_no_context_takeover"
|
|
12
|
+
_SERVER_MAX_WINDOW_BITS = b"server_max_window_bits"
|
|
13
|
+
_CLIENT_MAX_WINDOW_BITS = b"client_max_window_bits"
|
|
14
|
+
|
|
15
|
+
_VALID_PMD_PARAMETERS = {
|
|
16
|
+
_SERVER_NO_CONTEXT_TAKEOVER,
|
|
17
|
+
_CLIENT_NO_CONTEXT_TAKEOVER,
|
|
18
|
+
_SERVER_MAX_WINDOW_BITS,
|
|
19
|
+
_CLIENT_MAX_WINDOW_BITS,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _split_quoted(value: bytes, delimiter: int) -> list[bytes]:
|
|
24
|
+
parts: list[bytes] = []
|
|
25
|
+
buf = bytearray()
|
|
26
|
+
in_quote = False
|
|
27
|
+
escape = False
|
|
28
|
+
for byte in value:
|
|
29
|
+
if escape:
|
|
30
|
+
buf.append(byte)
|
|
31
|
+
escape = False
|
|
32
|
+
continue
|
|
33
|
+
if in_quote:
|
|
34
|
+
if byte == 0x5C: # backslash
|
|
35
|
+
escape = True
|
|
36
|
+
continue
|
|
37
|
+
if byte == 0x22:
|
|
38
|
+
in_quote = False
|
|
39
|
+
buf.append(byte)
|
|
40
|
+
continue
|
|
41
|
+
if byte == 0x22:
|
|
42
|
+
in_quote = True
|
|
43
|
+
buf.append(byte)
|
|
44
|
+
continue
|
|
45
|
+
if byte == delimiter:
|
|
46
|
+
part = bytes(buf).strip()
|
|
47
|
+
if part:
|
|
48
|
+
parts.append(part)
|
|
49
|
+
buf.clear()
|
|
50
|
+
continue
|
|
51
|
+
buf.append(byte)
|
|
52
|
+
if in_quote:
|
|
53
|
+
raise ProtocolError('malformed websocket extension header')
|
|
54
|
+
part = bytes(buf).strip()
|
|
55
|
+
if part:
|
|
56
|
+
parts.append(part)
|
|
57
|
+
return parts
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_token_value(value: bytes) -> bytes:
|
|
61
|
+
raw = value.strip()
|
|
62
|
+
if len(raw) >= 2 and raw[:1] == raw[-1:] == b'"':
|
|
63
|
+
inner = raw[1:-1]
|
|
64
|
+
if b'"' in inner:
|
|
65
|
+
raise ProtocolError('malformed websocket extension parameter value')
|
|
66
|
+
return inner
|
|
67
|
+
return raw
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_window_bits(value: bytes) -> int:
|
|
71
|
+
if not value or (len(value) > 1 and value.startswith(b'0')):
|
|
72
|
+
raise ProtocolError('invalid permessage-deflate window bits parameter')
|
|
73
|
+
try:
|
|
74
|
+
bits = int(value.decode('ascii', 'strict'))
|
|
75
|
+
except UnicodeDecodeError as exc:
|
|
76
|
+
raise ProtocolError('invalid permessage-deflate window bits parameter') from exc
|
|
77
|
+
except ValueError as exc:
|
|
78
|
+
raise ProtocolError('invalid permessage-deflate window bits parameter') from exc
|
|
79
|
+
if not 8 <= bits <= 15:
|
|
80
|
+
raise ProtocolError('invalid permessage-deflate window bits parameter')
|
|
81
|
+
return bits
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(slots=True, frozen=True)
|
|
85
|
+
class PerMessageDeflateOffer:
|
|
86
|
+
server_no_context_takeover: bool = False
|
|
87
|
+
client_no_context_takeover: bool = False
|
|
88
|
+
server_max_window_bits: int | None = None
|
|
89
|
+
client_max_window_bits_requested: bool = False
|
|
90
|
+
client_max_window_bits: int | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(slots=True, frozen=True)
|
|
94
|
+
class PerMessageDeflateAgreement:
|
|
95
|
+
server_no_context_takeover: bool = False
|
|
96
|
+
client_no_context_takeover: bool = False
|
|
97
|
+
server_max_window_bits: int | None = None
|
|
98
|
+
client_max_window_bits: int | None = None
|
|
99
|
+
|
|
100
|
+
def as_header_value(self) -> bytes:
|
|
101
|
+
params = [b'permessage-deflate']
|
|
102
|
+
if self.server_no_context_takeover:
|
|
103
|
+
params.append(_SERVER_NO_CONTEXT_TAKEOVER)
|
|
104
|
+
if self.client_no_context_takeover:
|
|
105
|
+
params.append(_CLIENT_NO_CONTEXT_TAKEOVER)
|
|
106
|
+
if self.server_max_window_bits is not None:
|
|
107
|
+
params.append(_SERVER_MAX_WINDOW_BITS + b'=' + str(self.server_max_window_bits).encode('ascii'))
|
|
108
|
+
if self.client_max_window_bits is not None:
|
|
109
|
+
params.append(_CLIENT_MAX_WINDOW_BITS + b'=' + str(self.client_max_window_bits).encode('ascii'))
|
|
110
|
+
return b'; '.join(params)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PerMessageDeflateRuntime:
|
|
114
|
+
def __init__(self, agreement: PerMessageDeflateAgreement) -> None:
|
|
115
|
+
self.agreement = agreement
|
|
116
|
+
self._compressor: zlib.compressobj | None = None
|
|
117
|
+
self._decompressor: zlib.decompressobj | None = None
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _runtime_window_bits(bits: int | None) -> int:
|
|
121
|
+
if bits is None:
|
|
122
|
+
return 15
|
|
123
|
+
# Python's raw DEFLATE bindings reject 8 here; fall back to the smallest
|
|
124
|
+
# supported raw window while preserving interoperable decompression.
|
|
125
|
+
return max(bits, 9)
|
|
126
|
+
|
|
127
|
+
def _new_compressor(self) -> zlib.compressobj:
|
|
128
|
+
return zlib.compressobj(wbits=-self._runtime_window_bits(self.agreement.server_max_window_bits))
|
|
129
|
+
|
|
130
|
+
def _new_decompressor(self) -> zlib.decompressobj:
|
|
131
|
+
return zlib.decompressobj(wbits=-self._runtime_window_bits(self.agreement.client_max_window_bits))
|
|
132
|
+
|
|
133
|
+
def compress_message(self, payload: bytes) -> bytes:
|
|
134
|
+
if self._compressor is None or self.agreement.server_no_context_takeover:
|
|
135
|
+
self._compressor = self._new_compressor()
|
|
136
|
+
compressed = self._compressor.compress(payload) + self._compressor.flush(zlib.Z_SYNC_FLUSH)
|
|
137
|
+
if not compressed.endswith(b'\x00\x00\xff\xff'):
|
|
138
|
+
raise RuntimeError('unexpected permessage-deflate trailer')
|
|
139
|
+
if self.agreement.server_no_context_takeover:
|
|
140
|
+
self._compressor = None
|
|
141
|
+
return compressed[:-4]
|
|
142
|
+
|
|
143
|
+
def decompress_message(self, payload: bytes) -> bytes:
|
|
144
|
+
if self._decompressor is None or self.agreement.client_no_context_takeover:
|
|
145
|
+
self._decompressor = self._new_decompressor()
|
|
146
|
+
try:
|
|
147
|
+
data = self._decompressor.decompress(payload + b'\x00\x00\xff\xff')
|
|
148
|
+
except zlib.error as exc:
|
|
149
|
+
raise ProtocolError('invalid permessage-deflate payload') from exc
|
|
150
|
+
if self._decompressor.unconsumed_tail or self._decompressor.unused_data:
|
|
151
|
+
raise ProtocolError('invalid permessage-deflate payload')
|
|
152
|
+
if self.agreement.client_no_context_takeover:
|
|
153
|
+
self._decompressor = None
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _iter_extension_elements(values: list[bytes]) -> list[tuple[bytes, list[tuple[bytes, bytes | None]]]]:
|
|
158
|
+
joined = b', '.join(value.strip() for value in values if value.strip())
|
|
159
|
+
if not joined:
|
|
160
|
+
return []
|
|
161
|
+
elements: list[tuple[bytes, list[tuple[bytes, bytes | None]]]] = []
|
|
162
|
+
for item in _split_quoted(joined, 0x2C): # comma
|
|
163
|
+
parts = _split_quoted(item, 0x3B) # semicolon
|
|
164
|
+
if not parts:
|
|
165
|
+
continue
|
|
166
|
+
name = parts[0].strip().lower()
|
|
167
|
+
params: list[tuple[bytes, bytes | None]] = []
|
|
168
|
+
for raw_param in parts[1:]:
|
|
169
|
+
if not raw_param:
|
|
170
|
+
continue
|
|
171
|
+
if b'=' in raw_param:
|
|
172
|
+
raw_name, raw_value = raw_param.split(b'=', 1)
|
|
173
|
+
param_name = raw_name.strip().lower()
|
|
174
|
+
param_value = _parse_token_value(raw_value)
|
|
175
|
+
else:
|
|
176
|
+
param_name = raw_param.strip().lower()
|
|
177
|
+
param_value = None
|
|
178
|
+
if not param_name:
|
|
179
|
+
raise ProtocolError('malformed websocket extension parameter')
|
|
180
|
+
params.append((param_name, param_value))
|
|
181
|
+
elements.append((name, params))
|
|
182
|
+
return elements
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_permessage_deflate_offers(headers: list[tuple[bytes, bytes]]) -> list[PerMessageDeflateOffer]:
|
|
186
|
+
offers: list[PerMessageDeflateOffer] = []
|
|
187
|
+
for name, params in _iter_extension_elements(get_headers(headers, b'sec-websocket-extensions')):
|
|
188
|
+
if name != _PERMESSAGE_DEFLATE:
|
|
189
|
+
continue
|
|
190
|
+
try:
|
|
191
|
+
offers.append(_parse_offer_parameters(params))
|
|
192
|
+
except ProtocolError:
|
|
193
|
+
continue
|
|
194
|
+
return offers
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def default_permessage_deflate_agreement(offers: list[PerMessageDeflateOffer]) -> PerMessageDeflateAgreement | None:
|
|
200
|
+
"""Choose a default server agreement for a valid permessage-deflate offer set.
|
|
201
|
+
|
|
202
|
+
The server accepts the first valid offer and mirrors explicit window constraints so
|
|
203
|
+
the generated response header corresponds to the client offer across websocket
|
|
204
|
+
carriers, including HTTP/2 and HTTP/3 third-party clients that require explicit
|
|
205
|
+
parameter echoing.
|
|
206
|
+
"""
|
|
207
|
+
if not offers:
|
|
208
|
+
return None
|
|
209
|
+
offer = offers[0]
|
|
210
|
+
return PerMessageDeflateAgreement(
|
|
211
|
+
server_no_context_takeover=offer.server_no_context_takeover,
|
|
212
|
+
client_no_context_takeover=False,
|
|
213
|
+
server_max_window_bits=offer.server_max_window_bits,
|
|
214
|
+
client_max_window_bits=offer.client_max_window_bits if offer.client_max_window_bits_requested else None,
|
|
215
|
+
)
|
|
216
|
+
def negotiate_permessage_deflate(
|
|
217
|
+
*,
|
|
218
|
+
request_headers: list[tuple[bytes, bytes]],
|
|
219
|
+
response_headers: list[tuple[bytes, bytes]],
|
|
220
|
+
) -> PerMessageDeflateAgreement | None:
|
|
221
|
+
response_values = get_headers(response_headers, b'sec-websocket-extensions')
|
|
222
|
+
if not response_values:
|
|
223
|
+
return None
|
|
224
|
+
response_elements = _iter_extension_elements(response_values)
|
|
225
|
+
if len(response_elements) != 1 or response_elements[0][0] != _PERMESSAGE_DEFLATE:
|
|
226
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
227
|
+
agreement = _parse_response_parameters(response_elements[0][1])
|
|
228
|
+
offers = parse_permessage_deflate_offers(request_headers)
|
|
229
|
+
if not offers:
|
|
230
|
+
raise RuntimeError('websocket extension not offered by the client')
|
|
231
|
+
for offer in offers:
|
|
232
|
+
if _agreement_matches_offer(agreement, offer):
|
|
233
|
+
return agreement
|
|
234
|
+
raise RuntimeError('websocket extension negotiation does not correspond to a client offer')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _parse_offer_parameters(params: list[tuple[bytes, bytes | None]]) -> PerMessageDeflateOffer:
|
|
238
|
+
server_no_context_takeover = False
|
|
239
|
+
client_no_context_takeover = False
|
|
240
|
+
server_max_window_bits: int | None = None
|
|
241
|
+
client_max_window_bits_requested = False
|
|
242
|
+
client_max_window_bits: int | None = None
|
|
243
|
+
seen: set[bytes] = set()
|
|
244
|
+
for name, value in params:
|
|
245
|
+
if name not in _VALID_PMD_PARAMETERS or name in seen:
|
|
246
|
+
raise ProtocolError('invalid permessage-deflate offer')
|
|
247
|
+
seen.add(name)
|
|
248
|
+
if name == _SERVER_NO_CONTEXT_TAKEOVER:
|
|
249
|
+
if value is not None:
|
|
250
|
+
raise ProtocolError('invalid permessage-deflate offer')
|
|
251
|
+
server_no_context_takeover = True
|
|
252
|
+
continue
|
|
253
|
+
if name == _CLIENT_NO_CONTEXT_TAKEOVER:
|
|
254
|
+
if value is not None:
|
|
255
|
+
raise ProtocolError('invalid permessage-deflate offer')
|
|
256
|
+
client_no_context_takeover = True
|
|
257
|
+
continue
|
|
258
|
+
if name == _SERVER_MAX_WINDOW_BITS:
|
|
259
|
+
if value is None:
|
|
260
|
+
raise ProtocolError('invalid permessage-deflate offer')
|
|
261
|
+
server_max_window_bits = _parse_window_bits(value)
|
|
262
|
+
continue
|
|
263
|
+
if name == _CLIENT_MAX_WINDOW_BITS:
|
|
264
|
+
client_max_window_bits_requested = True
|
|
265
|
+
if value is not None:
|
|
266
|
+
client_max_window_bits = _parse_window_bits(value)
|
|
267
|
+
continue
|
|
268
|
+
return PerMessageDeflateOffer(
|
|
269
|
+
server_no_context_takeover=server_no_context_takeover,
|
|
270
|
+
client_no_context_takeover=client_no_context_takeover,
|
|
271
|
+
server_max_window_bits=server_max_window_bits,
|
|
272
|
+
client_max_window_bits_requested=client_max_window_bits_requested,
|
|
273
|
+
client_max_window_bits=client_max_window_bits,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _parse_response_parameters(params: list[tuple[bytes, bytes | None]]) -> PerMessageDeflateAgreement:
|
|
278
|
+
server_no_context_takeover = False
|
|
279
|
+
client_no_context_takeover = False
|
|
280
|
+
server_max_window_bits: int | None = None
|
|
281
|
+
client_max_window_bits: int | None = None
|
|
282
|
+
seen: set[bytes] = set()
|
|
283
|
+
for name, value in params:
|
|
284
|
+
if name not in _VALID_PMD_PARAMETERS or name in seen:
|
|
285
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
286
|
+
seen.add(name)
|
|
287
|
+
if name == _SERVER_NO_CONTEXT_TAKEOVER:
|
|
288
|
+
if value is not None:
|
|
289
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
290
|
+
server_no_context_takeover = True
|
|
291
|
+
continue
|
|
292
|
+
if name == _CLIENT_NO_CONTEXT_TAKEOVER:
|
|
293
|
+
if value is not None:
|
|
294
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
295
|
+
client_no_context_takeover = True
|
|
296
|
+
continue
|
|
297
|
+
if name == _SERVER_MAX_WINDOW_BITS:
|
|
298
|
+
if value is None:
|
|
299
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
300
|
+
server_max_window_bits = _parse_window_bits(value)
|
|
301
|
+
continue
|
|
302
|
+
if value is None:
|
|
303
|
+
raise RuntimeError('unsupported websocket extension negotiation')
|
|
304
|
+
client_max_window_bits = _parse_window_bits(value)
|
|
305
|
+
return PerMessageDeflateAgreement(
|
|
306
|
+
server_no_context_takeover=server_no_context_takeover,
|
|
307
|
+
client_no_context_takeover=client_no_context_takeover,
|
|
308
|
+
server_max_window_bits=server_max_window_bits,
|
|
309
|
+
client_max_window_bits=client_max_window_bits,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _agreement_matches_offer(agreement: PerMessageDeflateAgreement, offer: PerMessageDeflateOffer) -> bool:
|
|
314
|
+
if offer.server_no_context_takeover and not agreement.server_no_context_takeover:
|
|
315
|
+
return False
|
|
316
|
+
if offer.server_max_window_bits is not None:
|
|
317
|
+
if agreement.server_max_window_bits is None or agreement.server_max_window_bits > offer.server_max_window_bits:
|
|
318
|
+
return False
|
|
319
|
+
if agreement.client_max_window_bits is not None:
|
|
320
|
+
if not offer.client_max_window_bits_requested:
|
|
321
|
+
return False
|
|
322
|
+
if offer.client_max_window_bits is not None and agreement.client_max_window_bits > offer.client_max_window_bits:
|
|
323
|
+
return False
|
|
324
|
+
return True
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.errors import ProtocolError
|
|
7
|
+
from tigrcorn_core.types import StreamReaderLike
|
|
8
|
+
|
|
9
|
+
OP_CONT = 0x0
|
|
10
|
+
OP_TEXT = 0x1
|
|
11
|
+
OP_BINARY = 0x2
|
|
12
|
+
OP_CLOSE = 0x8
|
|
13
|
+
OP_PING = 0x9
|
|
14
|
+
OP_PONG = 0xA
|
|
15
|
+
_CONTROL_OPCODES = {OP_CLOSE, OP_PING, OP_PONG}
|
|
16
|
+
_DATA_OPCODES = {OP_CONT, OP_TEXT, OP_BINARY}
|
|
17
|
+
_VALID_OPCODES = _CONTROL_OPCODES | _DATA_OPCODES
|
|
18
|
+
_FORBIDDEN_CLOSE_CODES = {1004, 1005, 1006, 1015}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Frame:
|
|
23
|
+
fin: bool
|
|
24
|
+
opcode: int
|
|
25
|
+
payload: bytes
|
|
26
|
+
rsv1: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _mask_payload(mask_key: bytes, payload: bytes) -> bytes:
|
|
30
|
+
return bytes(b ^ mask_key[i % 4] for i, b in enumerate(payload))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_close_code(code: int) -> None:
|
|
34
|
+
if code < 1000 or code >= 5000:
|
|
35
|
+
raise ProtocolError('invalid close code')
|
|
36
|
+
if code in _FORBIDDEN_CLOSE_CODES:
|
|
37
|
+
raise ProtocolError('invalid close code')
|
|
38
|
+
if 1016 <= code <= 2999:
|
|
39
|
+
raise ProtocolError('invalid close code')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _validate_frame_semantics(fin: bool, opcode: int, payload_length: int) -> None:
|
|
43
|
+
if opcode not in _VALID_OPCODES:
|
|
44
|
+
raise ProtocolError('unsupported websocket opcode')
|
|
45
|
+
if opcode in _CONTROL_OPCODES:
|
|
46
|
+
if not fin:
|
|
47
|
+
raise ProtocolError('control frames must not be fragmented')
|
|
48
|
+
if payload_length > 125:
|
|
49
|
+
raise ProtocolError('control frame payload too large')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_frame_bytes(data: bytes, *, expect_masked: bool = False, max_payload_size: int | None = None, allow_rsv1: bool = False) -> Frame:
|
|
53
|
+
if len(data) < 2:
|
|
54
|
+
raise ProtocolError('incomplete websocket frame')
|
|
55
|
+
pos = 0
|
|
56
|
+
b1, b2 = data[pos], data[pos + 1]
|
|
57
|
+
pos += 2
|
|
58
|
+
fin = bool(b1 & 0x80)
|
|
59
|
+
rsv1 = bool(b1 & 0x40)
|
|
60
|
+
rsv = b1 & 0x70
|
|
61
|
+
opcode = b1 & 0x0F
|
|
62
|
+
masked = bool(b2 & 0x80)
|
|
63
|
+
length = b2 & 0x7F
|
|
64
|
+
if rsv & 0x30:
|
|
65
|
+
raise ProtocolError('RSV2/RSV3 bits are not supported')
|
|
66
|
+
if rsv1 and not allow_rsv1:
|
|
67
|
+
raise ProtocolError('RSV1 is not negotiated')
|
|
68
|
+
if expect_masked and not masked:
|
|
69
|
+
raise ProtocolError('client websocket frames must be masked')
|
|
70
|
+
if length == 126:
|
|
71
|
+
if len(data) < pos + 2:
|
|
72
|
+
raise ProtocolError('incomplete websocket frame')
|
|
73
|
+
length = struct.unpack('!H', data[pos : pos + 2])[0]
|
|
74
|
+
pos += 2
|
|
75
|
+
elif length == 127:
|
|
76
|
+
if len(data) < pos + 8:
|
|
77
|
+
raise ProtocolError('incomplete websocket frame')
|
|
78
|
+
length = struct.unpack('!Q', data[pos : pos + 8])[0]
|
|
79
|
+
pos += 8
|
|
80
|
+
_validate_frame_semantics(fin, opcode, length)
|
|
81
|
+
if max_payload_size is not None and length > max_payload_size:
|
|
82
|
+
raise ProtocolError('websocket frame exceeds configured max payload size')
|
|
83
|
+
mask_key = b''
|
|
84
|
+
if masked:
|
|
85
|
+
if len(data) < pos + 4:
|
|
86
|
+
raise ProtocolError('incomplete websocket frame')
|
|
87
|
+
mask_key = data[pos : pos + 4]
|
|
88
|
+
pos += 4
|
|
89
|
+
if len(data) < pos + length:
|
|
90
|
+
raise ProtocolError('incomplete websocket frame')
|
|
91
|
+
payload = data[pos : pos + length]
|
|
92
|
+
if masked:
|
|
93
|
+
payload = _mask_payload(mask_key, payload)
|
|
94
|
+
return Frame(fin=fin, opcode=opcode, payload=payload, rsv1=rsv1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def read_frame(reader: StreamReaderLike, *, max_payload_size: int, expect_masked: bool = True, allow_rsv1: bool = False) -> Frame:
|
|
98
|
+
header = await reader.readexactly(2)
|
|
99
|
+
b1, b2 = header[0], header[1]
|
|
100
|
+
fin = bool(b1 & 0x80)
|
|
101
|
+
rsv1 = bool(b1 & 0x40)
|
|
102
|
+
rsv = b1 & 0x70
|
|
103
|
+
opcode = b1 & 0x0F
|
|
104
|
+
masked = bool(b2 & 0x80)
|
|
105
|
+
length = b2 & 0x7F
|
|
106
|
+
if rsv & 0x30:
|
|
107
|
+
raise ProtocolError('RSV2/RSV3 bits are not supported')
|
|
108
|
+
if rsv1 and not allow_rsv1:
|
|
109
|
+
raise ProtocolError('RSV1 is not negotiated')
|
|
110
|
+
if expect_masked and not masked:
|
|
111
|
+
raise ProtocolError('client websocket frames must be masked')
|
|
112
|
+
if length == 126:
|
|
113
|
+
length = struct.unpack('!H', await reader.readexactly(2))[0]
|
|
114
|
+
elif length == 127:
|
|
115
|
+
length = struct.unpack('!Q', await reader.readexactly(8))[0]
|
|
116
|
+
_validate_frame_semantics(fin, opcode, length)
|
|
117
|
+
if length > max_payload_size:
|
|
118
|
+
raise ProtocolError('websocket frame exceeds configured max payload size')
|
|
119
|
+
if masked:
|
|
120
|
+
mask_key = await reader.readexactly(4)
|
|
121
|
+
payload = await reader.readexactly(length)
|
|
122
|
+
payload = _mask_payload(mask_key, payload)
|
|
123
|
+
else:
|
|
124
|
+
payload = await reader.readexactly(length)
|
|
125
|
+
return Frame(fin=fin, opcode=opcode, payload=payload, rsv1=rsv1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def serialize_frame(opcode: int, payload: bytes = b'', *, fin: bool = True, mask: bool = False, mask_key: bytes = b'\x00\x00\x00\x00', rsv1: bool = False) -> bytes:
|
|
129
|
+
_validate_frame_semantics(fin, opcode, len(payload))
|
|
130
|
+
first = opcode | (0x80 if fin else 0) | (0x40 if rsv1 else 0)
|
|
131
|
+
length = len(payload)
|
|
132
|
+
mask_bit = 0x80 if mask else 0
|
|
133
|
+
if length < 126:
|
|
134
|
+
head = bytes([first, mask_bit | length])
|
|
135
|
+
elif length <= 0xFFFF:
|
|
136
|
+
head = bytes([first, mask_bit | 126]) + struct.pack('!H', length)
|
|
137
|
+
else:
|
|
138
|
+
head = bytes([first, mask_bit | 127]) + struct.pack('!Q', length)
|
|
139
|
+
if not mask:
|
|
140
|
+
return head + payload
|
|
141
|
+
masked = _mask_payload(mask_key, payload)
|
|
142
|
+
return head + mask_key + masked
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_frame(opcode: int, payload: bytes = b'', *, fin: bool = True, masked: bool = False, mask_key: bytes = b'\x00\x00\x00\x00', rsv1: bool = False) -> bytes:
|
|
146
|
+
return serialize_frame(opcode, payload, fin=fin, mask=masked, mask_key=mask_key, rsv1=rsv1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def decode_frame(data: bytes, *, expect_masked: bool = False, allow_rsv1: bool = False) -> Frame:
|
|
150
|
+
return parse_frame_bytes(data, expect_masked=expect_masked, allow_rsv1=allow_rsv1)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def encode_close_payload(code: int, reason: str = '') -> bytes:
|
|
154
|
+
validate_close_code(code)
|
|
155
|
+
encoded = reason.encode('utf-8')
|
|
156
|
+
if len(encoded) > 123:
|
|
157
|
+
raise ProtocolError('close reason too long')
|
|
158
|
+
return struct.pack('!H', code) + encoded if encoded or code != 1005 else b''
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def decode_close_payload(payload: bytes) -> tuple[int, str]:
|
|
162
|
+
if not payload:
|
|
163
|
+
return 1005, ''
|
|
164
|
+
if len(payload) == 1:
|
|
165
|
+
raise ProtocolError('invalid close payload')
|
|
166
|
+
if len(payload) > 125:
|
|
167
|
+
raise ProtocolError('control frame payload too large')
|
|
168
|
+
code = struct.unpack('!H', payload[:2])[0]
|
|
169
|
+
validate_close_code(code)
|
|
170
|
+
try:
|
|
171
|
+
reason = payload[2:].decode('utf-8', 'strict')
|
|
172
|
+
except UnicodeDecodeError as exc:
|
|
173
|
+
raise ProtocolError('invalid close reason utf-8') from exc
|
|
174
|
+
return code, reason
|