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,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from time import monotonic
|
|
7
|
+
|
|
8
|
+
from tigrcorn_asgi.events.websocket import (
|
|
9
|
+
websocket_connect,
|
|
10
|
+
websocket_disconnect,
|
|
11
|
+
websocket_receive_bytes,
|
|
12
|
+
websocket_receive_text,
|
|
13
|
+
)
|
|
14
|
+
from tigrcorn_asgi.receive import QueueReceive
|
|
15
|
+
from tigrcorn_asgi.scopes.websocket import build_websocket_scope
|
|
16
|
+
from tigrcorn_config.model import ServerConfig
|
|
17
|
+
from tigrcorn_core.errors import ProtocolError
|
|
18
|
+
from tigrcorn_observability.logging import AccessLogger
|
|
19
|
+
from tigrcorn_observability.metrics import Metrics
|
|
20
|
+
from tigrcorn_protocols.http1.serializer import serialize_http11_response_head, serialize_http11_response_whole
|
|
21
|
+
from tigrcorn_protocols.websocket.codec import binary_frame, close_frame, pong_frame, text_frame
|
|
22
|
+
from tigrcorn_protocols.websocket.frames import serialize_frame
|
|
23
|
+
from tigrcorn_protocols.websocket.frames import (
|
|
24
|
+
OP_BINARY,
|
|
25
|
+
OP_CLOSE,
|
|
26
|
+
OP_CONT,
|
|
27
|
+
OP_PING,
|
|
28
|
+
OP_PONG,
|
|
29
|
+
OP_TEXT,
|
|
30
|
+
decode_close_payload,
|
|
31
|
+
read_frame,
|
|
32
|
+
)
|
|
33
|
+
from tigrcorn_protocols.websocket.extensions import PerMessageDeflateRuntime, default_permessage_deflate_agreement, negotiate_permessage_deflate, parse_permessage_deflate_offers
|
|
34
|
+
from tigrcorn_protocols.websocket.handshake import build_handshake_response, validate_client_handshake
|
|
35
|
+
from tigrcorn_protocols.flow.keepalive import KeepAlivePolicy, KeepAliveRuntime
|
|
36
|
+
from tigrcorn_core.types import ASGIApp
|
|
37
|
+
from tigrcorn_core.utils.headers import get_header
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _WebSocketCloseSignal(Exception):
|
|
41
|
+
def __init__(self, code: int, reason: str) -> None:
|
|
42
|
+
super().__init__(reason)
|
|
43
|
+
self.code = code
|
|
44
|
+
self.reason = reason
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class _WSAppSend:
|
|
49
|
+
writer: asyncio.StreamWriter
|
|
50
|
+
server_header: bytes | None
|
|
51
|
+
state: dict
|
|
52
|
+
accepted: asyncio.Event
|
|
53
|
+
allowed_subprotocols: list[str] = field(default_factory=list)
|
|
54
|
+
include_date_header: bool = True
|
|
55
|
+
default_headers: list[tuple[bytes, bytes]] = field(default_factory=list)
|
|
56
|
+
config: ServerConfig | None = None
|
|
57
|
+
write_lock: asyncio.Lock | None = None
|
|
58
|
+
keepalive: KeepAliveRuntime | None = None
|
|
59
|
+
|
|
60
|
+
async def _write(self, data: bytes) -> None:
|
|
61
|
+
if self.write_lock is None:
|
|
62
|
+
self.writer.write(data)
|
|
63
|
+
self._record_activity()
|
|
64
|
+
return
|
|
65
|
+
async with self.write_lock:
|
|
66
|
+
self.writer.write(data)
|
|
67
|
+
await self.writer.drain()
|
|
68
|
+
|
|
69
|
+
def _record_activity(self) -> None:
|
|
70
|
+
if self.keepalive is not None:
|
|
71
|
+
self.keepalive.record_activity()
|
|
72
|
+
|
|
73
|
+
async def __call__(self, message: dict) -> None:
|
|
74
|
+
typ = message['type']
|
|
75
|
+
if typ == 'websocket.accept':
|
|
76
|
+
if self.state['accepted'] or self.state['http_denied']:
|
|
77
|
+
raise RuntimeError('websocket.accept sent more than once')
|
|
78
|
+
subprotocol = message.get('subprotocol')
|
|
79
|
+
if subprotocol is not None and subprotocol not in self.allowed_subprotocols:
|
|
80
|
+
raise RuntimeError('websocket.accept selected a subprotocol not offered by the client')
|
|
81
|
+
headers = [(bytes(k).lower(), bytes(v)) for k, v in message.get('headers', [])]
|
|
82
|
+
if get_header(headers, b'sec-websocket-extensions') is not None:
|
|
83
|
+
raise RuntimeError('websocket.accept must not override extension negotiation headers directly')
|
|
84
|
+
compression_mode = self.config.websocket.compression if self.config is not None else 'off'
|
|
85
|
+
if compression_mode == 'permessage-deflate' and self.state.get('permessage_deflate_offers'):
|
|
86
|
+
default_agreement = default_permessage_deflate_agreement(self.state.get('permessage_deflate_offers') or [])
|
|
87
|
+
if default_agreement is not None:
|
|
88
|
+
headers = headers + [(b'sec-websocket-extensions', default_agreement.as_header_value())]
|
|
89
|
+
negotiated_extensions: list[tuple[bytes, bytes]] = []
|
|
90
|
+
agreement = negotiate_permessage_deflate(
|
|
91
|
+
request_headers=self.state.get('request_headers', []),
|
|
92
|
+
response_headers=headers,
|
|
93
|
+
)
|
|
94
|
+
if agreement is not None:
|
|
95
|
+
negotiated_extensions.append((b'sec-websocket-extensions', agreement.as_header_value()))
|
|
96
|
+
self.state['permessage_deflate_runtime'] = PerMessageDeflateRuntime(agreement)
|
|
97
|
+
if get_header(headers, b'sec-websocket-protocol') is not None:
|
|
98
|
+
raise RuntimeError('use websocket.accept subprotocol instead of sec-websocket-protocol response headers')
|
|
99
|
+
payload = build_handshake_response(
|
|
100
|
+
self.state['sec_websocket_key'],
|
|
101
|
+
subprotocol=subprotocol,
|
|
102
|
+
headers=[(k, v) for k, v in headers if k != b'sec-websocket-extensions'] + negotiated_extensions,
|
|
103
|
+
server_header=self.server_header,
|
|
104
|
+
include_date_header=self.include_date_header,
|
|
105
|
+
default_headers=self.default_headers,
|
|
106
|
+
)
|
|
107
|
+
await self._write(payload)
|
|
108
|
+
self._record_activity()
|
|
109
|
+
self.state['accepted'] = True
|
|
110
|
+
self.accepted.set()
|
|
111
|
+
return
|
|
112
|
+
if typ == 'websocket.send':
|
|
113
|
+
if not self.state['accepted']:
|
|
114
|
+
raise RuntimeError('websocket.send before websocket.accept')
|
|
115
|
+
if self.state['closed']:
|
|
116
|
+
return
|
|
117
|
+
text = message.get('text')
|
|
118
|
+
data = message.get('bytes')
|
|
119
|
+
if text is not None and data is not None:
|
|
120
|
+
raise RuntimeError('websocket.send cannot contain both text and bytes')
|
|
121
|
+
if text is not None:
|
|
122
|
+
runtime = self.state.get('permessage_deflate_runtime')
|
|
123
|
+
if runtime is not None:
|
|
124
|
+
await self._write(serialize_frame(OP_TEXT, runtime.compress_message(text.encode('utf-8')), rsv1=True))
|
|
125
|
+
else:
|
|
126
|
+
await self._write(text_frame(text))
|
|
127
|
+
else:
|
|
128
|
+
raw = data or b''
|
|
129
|
+
runtime = self.state.get('permessage_deflate_runtime')
|
|
130
|
+
if runtime is not None:
|
|
131
|
+
await self._write(binary_frame(runtime.compress_message(raw), rsv1=True))
|
|
132
|
+
else:
|
|
133
|
+
await self._write(binary_frame(raw))
|
|
134
|
+
self._record_activity()
|
|
135
|
+
return
|
|
136
|
+
if typ == 'websocket.close':
|
|
137
|
+
code = int(message.get('code', 1000))
|
|
138
|
+
reason = message.get('reason', '')
|
|
139
|
+
if not self.state['accepted']:
|
|
140
|
+
await self._write(
|
|
141
|
+
serialize_http11_response_whole(
|
|
142
|
+
status=403,
|
|
143
|
+
headers=[],
|
|
144
|
+
body=b'',
|
|
145
|
+
keep_alive=False,
|
|
146
|
+
server_header=self.server_header,
|
|
147
|
+
include_date_header=self.include_date_header,
|
|
148
|
+
default_headers=self.default_headers,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
self.state['http_denied'] = True
|
|
152
|
+
self.state['closed'] = True
|
|
153
|
+
return
|
|
154
|
+
if not self.state['closed']:
|
|
155
|
+
await self._write(close_frame(code, reason))
|
|
156
|
+
self.state['closed'] = True
|
|
157
|
+
return
|
|
158
|
+
if typ == 'websocket.http.response.start':
|
|
159
|
+
if self.state['accepted']:
|
|
160
|
+
raise RuntimeError('cannot send websocket.http.response.start after accept')
|
|
161
|
+
self.state['http_denial_status'] = int(message['status'])
|
|
162
|
+
self.state['http_denial_headers'] = list(message.get('headers', []))
|
|
163
|
+
self.state['http_denied'] = True
|
|
164
|
+
return
|
|
165
|
+
if typ == 'websocket.http.response.body':
|
|
166
|
+
if not self.state['http_denied']:
|
|
167
|
+
raise RuntimeError('websocket.http.response.body before denial start')
|
|
168
|
+
body = message.get('body', b'')
|
|
169
|
+
more = bool(message.get('more_body', False))
|
|
170
|
+
if not self.state['http_denial_started']:
|
|
171
|
+
if more:
|
|
172
|
+
head = serialize_http11_response_head(
|
|
173
|
+
status=self.state['http_denial_status'],
|
|
174
|
+
headers=self.state['http_denial_headers'],
|
|
175
|
+
keep_alive=False,
|
|
176
|
+
server_header=self.server_header,
|
|
177
|
+
chunked=True,
|
|
178
|
+
include_date_header=self.include_date_header,
|
|
179
|
+
default_headers=self.default_headers,
|
|
180
|
+
)
|
|
181
|
+
await self._write(head + (f'{len(body):X}'.encode('ascii') + b'\r\n' + body + b'\r\n' if body else b''))
|
|
182
|
+
else:
|
|
183
|
+
await self._write(
|
|
184
|
+
serialize_http11_response_whole(
|
|
185
|
+
status=self.state['http_denial_status'],
|
|
186
|
+
headers=self.state['http_denial_headers'],
|
|
187
|
+
body=body,
|
|
188
|
+
keep_alive=False,
|
|
189
|
+
server_header=self.server_header,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
self.state['closed'] = True
|
|
193
|
+
self.state['http_denial_started'] = True
|
|
194
|
+
else:
|
|
195
|
+
if body:
|
|
196
|
+
await self._write(f'{len(body):X}'.encode('ascii') + b'\r\n' + body + b'\r\n')
|
|
197
|
+
if not more:
|
|
198
|
+
await self._write(b'0\r\n\r\n')
|
|
199
|
+
self.state['closed'] = True
|
|
200
|
+
self._record_activity()
|
|
201
|
+
return
|
|
202
|
+
raise RuntimeError(f'unexpected websocket send message: {typ!r}')
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class WebSocketConnectionHandler:
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
*,
|
|
209
|
+
app: ASGIApp,
|
|
210
|
+
config: ServerConfig,
|
|
211
|
+
access_logger: AccessLogger,
|
|
212
|
+
request,
|
|
213
|
+
reader,
|
|
214
|
+
writer,
|
|
215
|
+
client,
|
|
216
|
+
server,
|
|
217
|
+
scheme: str,
|
|
218
|
+
scope_extensions: dict | None = None,
|
|
219
|
+
metrics: Metrics | None = None,
|
|
220
|
+
) -> None:
|
|
221
|
+
self.app = app
|
|
222
|
+
self.config = config
|
|
223
|
+
self.access_logger = access_logger
|
|
224
|
+
self.request = request
|
|
225
|
+
self.reader = reader
|
|
226
|
+
self.writer = writer
|
|
227
|
+
self.client = client
|
|
228
|
+
self.server = server
|
|
229
|
+
self.scheme = scheme
|
|
230
|
+
self.scope_extensions = dict(scope_extensions or {})
|
|
231
|
+
self.metrics = metrics
|
|
232
|
+
self.receive = QueueReceive(max_size=self.config.websocket.max_queue)
|
|
233
|
+
self.accepted = asyncio.Event()
|
|
234
|
+
self.write_lock = asyncio.Lock()
|
|
235
|
+
self.keepalive_policy = KeepAlivePolicy(
|
|
236
|
+
idle_timeout=self.config.http.idle_timeout,
|
|
237
|
+
ping_interval=self.config.websocket.ping_interval,
|
|
238
|
+
ping_timeout=self.config.websocket.ping_timeout,
|
|
239
|
+
)
|
|
240
|
+
self.keepalive = KeepAliveRuntime(self.keepalive_policy) if self.keepalive_policy.enabled else None
|
|
241
|
+
self.keepalive_task: asyncio.Task[None] | None = None
|
|
242
|
+
self.state = {
|
|
243
|
+
'accepted': False,
|
|
244
|
+
'closed': False,
|
|
245
|
+
'http_denied': False,
|
|
246
|
+
'http_denial_status': 403,
|
|
247
|
+
'http_denial_headers': [],
|
|
248
|
+
'http_denial_started': False,
|
|
249
|
+
'sec_websocket_key': validate_client_handshake(request.headers),
|
|
250
|
+
'request_headers': request.headers,
|
|
251
|
+
'permessage_deflate_offers': parse_permessage_deflate_offers(request.headers),
|
|
252
|
+
'permessage_deflate_runtime': None,
|
|
253
|
+
}
|
|
254
|
+
self.send = _WSAppSend(
|
|
255
|
+
writer=writer,
|
|
256
|
+
server_header=config.server_header_value,
|
|
257
|
+
state=self.state,
|
|
258
|
+
accepted=self.accepted,
|
|
259
|
+
allowed_subprotocols=build_websocket_scope(
|
|
260
|
+
self.request,
|
|
261
|
+
client=self.client,
|
|
262
|
+
server=self.server,
|
|
263
|
+
scheme=self.scheme,
|
|
264
|
+
extensions=self.scope_extensions,
|
|
265
|
+
root_path=self.config.proxy.root_path,
|
|
266
|
+
proxy=self.config.proxy,
|
|
267
|
+
)['subprotocols'],
|
|
268
|
+
include_date_header=config.include_date_header,
|
|
269
|
+
default_headers=list(config.default_response_headers),
|
|
270
|
+
config=config,
|
|
271
|
+
write_lock=self.write_lock,
|
|
272
|
+
keepalive=self.keepalive,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def handle(self) -> None:
|
|
276
|
+
scope = build_websocket_scope(
|
|
277
|
+
self.request,
|
|
278
|
+
client=self.client,
|
|
279
|
+
server=self.server,
|
|
280
|
+
scheme=self.scheme,
|
|
281
|
+
extensions=self.scope_extensions,
|
|
282
|
+
root_path=self.config.proxy.root_path,
|
|
283
|
+
proxy=self.config.proxy,
|
|
284
|
+
)
|
|
285
|
+
self.send.allowed_subprotocols = scope['subprotocols']
|
|
286
|
+
await self.receive.put(websocket_connect())
|
|
287
|
+
reader_task = asyncio.create_task(self._frame_reader(), name='tigrcorn-ws-reader')
|
|
288
|
+
if self.keepalive is not None:
|
|
289
|
+
self.keepalive_task = asyncio.create_task(self._keepalive_loop(), name='tigrcorn-ws-keepalive')
|
|
290
|
+
try:
|
|
291
|
+
await self.app(scope, self.receive, self.send)
|
|
292
|
+
except Exception:
|
|
293
|
+
if self.state['accepted'] and not self.state['closed']:
|
|
294
|
+
with suppress(Exception):
|
|
295
|
+
await self._write(close_frame(1011, 'internal error'))
|
|
296
|
+
raise
|
|
297
|
+
finally:
|
|
298
|
+
if not self.state['accepted'] and not self.state['http_denied']:
|
|
299
|
+
await self._write(
|
|
300
|
+
serialize_http11_response_whole(
|
|
301
|
+
status=403,
|
|
302
|
+
headers=[],
|
|
303
|
+
body=b'',
|
|
304
|
+
keep_alive=False,
|
|
305
|
+
server_header=self.config.server_header_value,
|
|
306
|
+
include_date_header=self.config.include_date_header,
|
|
307
|
+
default_headers=self.config.default_response_headers,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
self.state['closed'] = True
|
|
311
|
+
elif self.state['http_denied'] and not self.state['http_denial_started']:
|
|
312
|
+
await self._write(
|
|
313
|
+
serialize_http11_response_whole(
|
|
314
|
+
status=self.state['http_denial_status'],
|
|
315
|
+
headers=self.state['http_denial_headers'],
|
|
316
|
+
body=b'',
|
|
317
|
+
keep_alive=False,
|
|
318
|
+
server_header=self.config.server_header_value,
|
|
319
|
+
include_date_header=self.config.include_date_header,
|
|
320
|
+
default_headers=self.config.default_response_headers,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
self.state['closed'] = True
|
|
324
|
+
elif self.state['accepted'] and not self.state['closed']:
|
|
325
|
+
await self._write(close_frame(1000, ''))
|
|
326
|
+
self.state['closed'] = True
|
|
327
|
+
if self.keepalive_task is not None:
|
|
328
|
+
self.keepalive_task.cancel()
|
|
329
|
+
with suppress(Exception):
|
|
330
|
+
await self.keepalive_task
|
|
331
|
+
reader_task.cancel()
|
|
332
|
+
with suppress(Exception):
|
|
333
|
+
await reader_task
|
|
334
|
+
self.access_logger.log_ws(self.client, self.request.path, 'accepted' if self.state['accepted'] else 'denied')
|
|
335
|
+
|
|
336
|
+
async def _write(self, data: bytes) -> None:
|
|
337
|
+
async with self.write_lock:
|
|
338
|
+
self.writer.write(data)
|
|
339
|
+
await self.writer.drain()
|
|
340
|
+
|
|
341
|
+
def _record_activity(self) -> None:
|
|
342
|
+
if self.keepalive is not None:
|
|
343
|
+
self.keepalive.record_activity()
|
|
344
|
+
|
|
345
|
+
async def _keepalive_loop(self) -> None:
|
|
346
|
+
await self.accepted.wait()
|
|
347
|
+
while not self.state['closed']:
|
|
348
|
+
await asyncio.sleep(0.05)
|
|
349
|
+
if self.keepalive is None or self.state['closed']:
|
|
350
|
+
return
|
|
351
|
+
if self.keepalive.ping_timed_out():
|
|
352
|
+
if self.metrics is not None:
|
|
353
|
+
self.metrics.websocket_ping_timeout()
|
|
354
|
+
await self._fail_connection(1011, 'ping timeout')
|
|
355
|
+
return
|
|
356
|
+
payload = self.keepalive.next_ping_payload()
|
|
357
|
+
if payload is None:
|
|
358
|
+
continue
|
|
359
|
+
if self.metrics is not None:
|
|
360
|
+
self.metrics.websocket_ping_sent()
|
|
361
|
+
await self._write(serialize_frame(OP_PING, payload))
|
|
362
|
+
|
|
363
|
+
def _ensure_message_size(self, size: int) -> None:
|
|
364
|
+
if size > self.config.websocket_max_message_size:
|
|
365
|
+
raise _WebSocketCloseSignal(1009, 'message too big')
|
|
366
|
+
|
|
367
|
+
async def _fail_connection(self, code: int, reason: str) -> None:
|
|
368
|
+
if not self.state['closed']:
|
|
369
|
+
await self._write(close_frame(code, reason))
|
|
370
|
+
await self.receive.put(websocket_disconnect(code, reason))
|
|
371
|
+
self.state['closed'] = True
|
|
372
|
+
|
|
373
|
+
async def _frame_reader(self) -> None:
|
|
374
|
+
await self.accepted.wait()
|
|
375
|
+
fragmented_opcode: int | None = None
|
|
376
|
+
fragments: list[bytes] = []
|
|
377
|
+
fragmented_compressed = False
|
|
378
|
+
current_message_size = 0
|
|
379
|
+
while not self.state['closed']:
|
|
380
|
+
try:
|
|
381
|
+
frame = await read_frame(
|
|
382
|
+
self.reader,
|
|
383
|
+
max_payload_size=self.config.websocket_max_message_size,
|
|
384
|
+
allow_rsv1=self.state.get('permessage_deflate_runtime') is not None,
|
|
385
|
+
)
|
|
386
|
+
self._record_activity()
|
|
387
|
+
if frame.opcode == OP_PING:
|
|
388
|
+
await self._write(pong_frame(frame.payload))
|
|
389
|
+
continue
|
|
390
|
+
if frame.opcode == OP_PONG:
|
|
391
|
+
if self.keepalive is not None:
|
|
392
|
+
self.keepalive.acknowledge_pong(frame.payload)
|
|
393
|
+
continue
|
|
394
|
+
if frame.opcode == OP_CLOSE:
|
|
395
|
+
code, reason = decode_close_payload(frame.payload)
|
|
396
|
+
if not self.state['closed']:
|
|
397
|
+
await self._write(close_frame(code, reason))
|
|
398
|
+
self.state['closed'] = True
|
|
399
|
+
await self.receive.put(websocket_disconnect(code, reason))
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
opcode = frame.opcode
|
|
403
|
+
if opcode in {OP_TEXT, OP_BINARY}:
|
|
404
|
+
if fragmented_opcode is not None:
|
|
405
|
+
raise ProtocolError('new data frame before fragmented message completion')
|
|
406
|
+
current_message_size = len(frame.payload)
|
|
407
|
+
self._ensure_message_size(current_message_size)
|
|
408
|
+
fragmented_opcode = opcode if not frame.fin else None
|
|
409
|
+
fragmented_compressed = frame.rsv1
|
|
410
|
+
if frame.fin:
|
|
411
|
+
runtime = self.state.get('permessage_deflate_runtime')
|
|
412
|
+
payload = runtime.decompress_message(frame.payload) if frame.rsv1 and runtime is not None else frame.payload
|
|
413
|
+
await self._deliver_message(opcode, payload)
|
|
414
|
+
current_message_size = 0
|
|
415
|
+
else:
|
|
416
|
+
fragments = [frame.payload]
|
|
417
|
+
continue
|
|
418
|
+
if opcode == OP_CONT:
|
|
419
|
+
if fragmented_opcode is None:
|
|
420
|
+
raise ProtocolError('unexpected continuation frame')
|
|
421
|
+
if frame.rsv1:
|
|
422
|
+
raise ProtocolError('RSV1 is only valid on the first frame of a compressed message')
|
|
423
|
+
current_message_size += len(frame.payload)
|
|
424
|
+
self._ensure_message_size(current_message_size)
|
|
425
|
+
fragments.append(frame.payload)
|
|
426
|
+
if frame.fin:
|
|
427
|
+
payload = b''.join(fragments)
|
|
428
|
+
if fragmented_compressed:
|
|
429
|
+
runtime = self.state.get('permessage_deflate_runtime')
|
|
430
|
+
if runtime is None:
|
|
431
|
+
raise ProtocolError('RSV1 is not negotiated')
|
|
432
|
+
payload = runtime.decompress_message(payload)
|
|
433
|
+
opcode = fragmented_opcode
|
|
434
|
+
fragmented_opcode = None
|
|
435
|
+
fragments = []
|
|
436
|
+
fragmented_compressed = False
|
|
437
|
+
current_message_size = 0
|
|
438
|
+
await self._deliver_message(opcode, payload)
|
|
439
|
+
continue
|
|
440
|
+
raise ProtocolError('unsupported websocket opcode')
|
|
441
|
+
except asyncio.CancelledError:
|
|
442
|
+
raise
|
|
443
|
+
except _WebSocketCloseSignal as exc:
|
|
444
|
+
await self._fail_connection(exc.code, exc.reason)
|
|
445
|
+
return
|
|
446
|
+
except ProtocolError:
|
|
447
|
+
await self._fail_connection(1002, 'protocol error')
|
|
448
|
+
return
|
|
449
|
+
except Exception:
|
|
450
|
+
await self.receive.put(websocket_disconnect(1006, ''))
|
|
451
|
+
self.state['closed'] = True
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
async def _deliver_message(self, opcode: int, payload: bytes) -> None:
|
|
455
|
+
if opcode == OP_TEXT:
|
|
456
|
+
try:
|
|
457
|
+
text = payload.decode('utf-8', 'strict')
|
|
458
|
+
except UnicodeDecodeError as exc:
|
|
459
|
+
raise _WebSocketCloseSignal(1007, 'invalid frame payload data') from exc
|
|
460
|
+
await self.receive.put(websocket_receive_text(text))
|
|
461
|
+
return
|
|
462
|
+
await self.receive.put(websocket_receive_bytes(payload))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.errors import ProtocolError
|
|
7
|
+
from tigrcorn_core.utils.headers import apply_response_header_policy, get_header, header_contains_token
|
|
8
|
+
|
|
9
|
+
_MAGIC = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_websocket_upgrade(method: str, headers: list[tuple[bytes, bytes]]) -> bool:
|
|
13
|
+
return (
|
|
14
|
+
method.upper() == "GET"
|
|
15
|
+
and header_contains_token(headers, b"connection", b"upgrade")
|
|
16
|
+
and header_contains_token(headers, b"upgrade", b"websocket")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def websocket_accept_value(sec_websocket_key: bytes) -> bytes:
|
|
21
|
+
sha = hashlib.sha1(sec_websocket_key + _MAGIC).digest()
|
|
22
|
+
return base64.b64encode(sha)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_client_handshake(headers: list[tuple[bytes, bytes]]) -> bytes:
|
|
26
|
+
version = get_header(headers, b"sec-websocket-version")
|
|
27
|
+
if version != b"13":
|
|
28
|
+
raise ProtocolError("unsupported websocket version")
|
|
29
|
+
key = get_header(headers, b"sec-websocket-key")
|
|
30
|
+
if not key:
|
|
31
|
+
raise ProtocolError("missing Sec-WebSocket-Key")
|
|
32
|
+
try:
|
|
33
|
+
decoded = base64.b64decode(key, validate=True)
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
raise ProtocolError("invalid Sec-WebSocket-Key") from exc
|
|
36
|
+
if len(decoded) != 16:
|
|
37
|
+
raise ProtocolError("invalid Sec-WebSocket-Key length")
|
|
38
|
+
return key
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_handshake_response(
|
|
42
|
+
sec_websocket_key: bytes,
|
|
43
|
+
*,
|
|
44
|
+
subprotocol: str | None = None,
|
|
45
|
+
headers: list[tuple[bytes, bytes]] | None = None,
|
|
46
|
+
server_header: bytes | None = None,
|
|
47
|
+
include_date_header: bool = True,
|
|
48
|
+
default_headers: list[tuple[bytes, bytes]] | tuple[tuple[bytes, bytes], ...] = (),
|
|
49
|
+
) -> bytes:
|
|
50
|
+
response_headers = [
|
|
51
|
+
(b"upgrade", b"websocket"),
|
|
52
|
+
(b"connection", b"Upgrade"),
|
|
53
|
+
(b"sec-websocket-accept", websocket_accept_value(sec_websocket_key)),
|
|
54
|
+
]
|
|
55
|
+
if subprotocol:
|
|
56
|
+
response_headers.append((b"sec-websocket-protocol", subprotocol.encode("ascii")))
|
|
57
|
+
if headers:
|
|
58
|
+
response_headers.extend([(k.lower(), v) for k, v in headers])
|
|
59
|
+
response_headers = apply_response_header_policy(
|
|
60
|
+
response_headers,
|
|
61
|
+
server_header=server_header,
|
|
62
|
+
include_date_header=include_date_header,
|
|
63
|
+
default_headers=default_headers,
|
|
64
|
+
)
|
|
65
|
+
lines = [b"HTTP/1.1 101 Switching Protocols"] + [k + b": " + v for k, v in response_headers]
|
|
66
|
+
return b"\r\n".join(lines) + b"\r\n\r\n"
|