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,1823 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tigrcorn_asgi.receive import HTTPRequestReceive, apply_request_trailer_policy
|
|
9
|
+
from tigrcorn_asgi.scopes.custom import build_custom_scope
|
|
10
|
+
from tigrcorn_asgi.scopes.http import build_http_scope
|
|
11
|
+
from tigrcorn_asgi.send import HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
|
|
12
|
+
from tigrcorn_config.model import ListenerConfig, ServerConfig
|
|
13
|
+
from tigrcorn_core.errors import ProtocolError
|
|
14
|
+
from tigrcorn_observability.logging import AccessLogger
|
|
15
|
+
from tigrcorn_observability.metrics import Metrics
|
|
16
|
+
from tigrcorn_security.tls import build_server_ssl_context
|
|
17
|
+
from tigrcorn_protocols.connect import close_tcp_writer, half_close_tcp_writer, is_connect_allowed, parse_connect_authority
|
|
18
|
+
from tigrcorn_protocols.custom.adapters import adapt_scope
|
|
19
|
+
from tigrcorn_protocols.http1.parser import ParsedRequest
|
|
20
|
+
from tigrcorn_http.alt_svc import configured_alt_svc_values
|
|
21
|
+
from tigrcorn_http.entity import apply_response_entity_semantics, plan_file_backed_response_entity_semantics
|
|
22
|
+
from tigrcorn_protocols.http3.codec import (
|
|
23
|
+
FRAME_DATA,
|
|
24
|
+
FRAME_HEADERS,
|
|
25
|
+
H3_CONNECT_ERROR,
|
|
26
|
+
H3_ID_ERROR,
|
|
27
|
+
H3_GENERAL_PROTOCOL_ERROR,
|
|
28
|
+
H3_REQUEST_CANCELLED,
|
|
29
|
+
SETTING_ENABLE_CONNECT_PROTOCOL,
|
|
30
|
+
SETTING_ENABLE_WEBTRANSPORT,
|
|
31
|
+
SETTING_H3_DATAGRAM,
|
|
32
|
+
HTTP3ConnectionError,
|
|
33
|
+
HTTP3StreamError,
|
|
34
|
+
encode_frame,
|
|
35
|
+
)
|
|
36
|
+
from tigrcorn_protocols.http3.streams import (
|
|
37
|
+
STREAM_TYPE_QPACK_DECODER,
|
|
38
|
+
STREAM_TYPE_QPACK_ENCODER,
|
|
39
|
+
HTTP3ConnectionCore,
|
|
40
|
+
)
|
|
41
|
+
from tigrcorn_protocols.http3.websocket import H3WebSocketSession
|
|
42
|
+
from tigrcorn_transports.quic.connection import QuicConnection
|
|
43
|
+
from tigrcorn_transports.quic.handshake import QuicTlsHandshakeDriver, TransportParameters
|
|
44
|
+
from tigrcorn_transports.quic.packets import QuicLongHeaderPacket, QuicLongHeaderType, QuicRetryPacket, QuicShortHeaderPacket, QuicVersionNegotiationPacket, decode_packet
|
|
45
|
+
from tigrcorn_transports.udp.endpoint import UDPEndpoint
|
|
46
|
+
from tigrcorn_transports.udp.packet import UDPPacket
|
|
47
|
+
from tigrcorn_core.types import ASGIApp
|
|
48
|
+
from tigrcorn_core.utils.bytes import decode_quic_varint, encode_quic_varint
|
|
49
|
+
from tigrcorn_core.utils.authority import authority_allowed
|
|
50
|
+
from tigrcorn_core.utils.headers import apply_response_header_policy, sanitize_early_hints_headers, strip_connection_specific_headers
|
|
51
|
+
|
|
52
|
+
from .webtransport import _HTTP3WebTransportSession
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class HTTP3Session:
|
|
57
|
+
addr: tuple[str, int]
|
|
58
|
+
quic: QuicConnection
|
|
59
|
+
h3: HTTP3ConnectionCore = field(default_factory=lambda: HTTP3ConnectionCore(role='server'))
|
|
60
|
+
server_control_stream_sent: bool = False
|
|
61
|
+
server_control_stream_id: int | None = None
|
|
62
|
+
responded_streams: set[int] = field(default_factory=set)
|
|
63
|
+
request_packets: int = 0
|
|
64
|
+
server_qpack_encoder_stream_id: int | None = None
|
|
65
|
+
server_qpack_decoder_stream_id: int | None = None
|
|
66
|
+
bytes_received: int = 0
|
|
67
|
+
bytes_sent: int = 0
|
|
68
|
+
address_validated: bool = False
|
|
69
|
+
session_ticket_issued: bool = False
|
|
70
|
+
pending_outbound: list[bytes] = field(default_factory=list)
|
|
71
|
+
timer_handle: asyncio.TimerHandle | None = None
|
|
72
|
+
connect_tunnels: dict[int, _HTTP3ConnectTunnel] = field(default_factory=dict)
|
|
73
|
+
websocket_sessions: dict[int, H3WebSocketSession] = field(default_factory=dict)
|
|
74
|
+
webtransport_sessions: dict[int, _HTTP3WebTransportSession] = field(default_factory=dict)
|
|
75
|
+
webtransport_streams: set[int] = field(default_factory=set)
|
|
76
|
+
webtransport_stream_owners: dict[int, int] = field(default_factory=dict)
|
|
77
|
+
webtransport_stream_prefaces: dict[int, bytearray] = field(default_factory=dict)
|
|
78
|
+
stream_work_leases: dict[int, object] = field(default_factory=dict)
|
|
79
|
+
early_data_accounted: bool = False
|
|
80
|
+
peer_goaway_observed: bool = False
|
|
81
|
+
last_quic_packets_lost_total: int = 0
|
|
82
|
+
last_quic_pto_expirations_total: int = 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _HTTP3ConnectTunnel:
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
handler: HTTP3DatagramHandler,
|
|
90
|
+
session: HTTP3Session,
|
|
91
|
+
stream_id: int,
|
|
92
|
+
authority: str,
|
|
93
|
+
endpoint: UDPEndpoint,
|
|
94
|
+
upstream_reader: asyncio.StreamReader,
|
|
95
|
+
upstream_writer: asyncio.StreamWriter,
|
|
96
|
+
work_lease: object | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
self.handler = handler
|
|
99
|
+
self.session = session
|
|
100
|
+
self.stream_id = stream_id
|
|
101
|
+
self.authority = authority
|
|
102
|
+
self.endpoint = endpoint
|
|
103
|
+
self.upstream_reader = upstream_reader
|
|
104
|
+
self.upstream_writer = upstream_writer
|
|
105
|
+
self.work_lease = work_lease
|
|
106
|
+
self.relay_task: asyncio.Task[None] | None = None
|
|
107
|
+
self.client_input_closed = False
|
|
108
|
+
self.server_output_closed = False
|
|
109
|
+
self.closed = False
|
|
110
|
+
|
|
111
|
+
def start(self) -> None:
|
|
112
|
+
self.relay_task = asyncio.create_task(
|
|
113
|
+
self._relay_upstream_to_client(),
|
|
114
|
+
name=f'tigrcorn-h3-connect-{self.stream_id}',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def feed_client_data(self, chunks: list[bytes], *, end_stream: bool, already_locked: bool = False) -> None:
|
|
118
|
+
if self.closed:
|
|
119
|
+
return
|
|
120
|
+
try:
|
|
121
|
+
wrote = False
|
|
122
|
+
for chunk in chunks:
|
|
123
|
+
if not chunk:
|
|
124
|
+
continue
|
|
125
|
+
self.upstream_writer.write(chunk)
|
|
126
|
+
wrote = True
|
|
127
|
+
if wrote:
|
|
128
|
+
await self.upstream_writer.drain()
|
|
129
|
+
if end_stream and not self.client_input_closed:
|
|
130
|
+
self.client_input_closed = True
|
|
131
|
+
await half_close_tcp_writer(self.upstream_writer)
|
|
132
|
+
except Exception:
|
|
133
|
+
await self.handler._reset_http3_tunnel_stream(
|
|
134
|
+
self.session,
|
|
135
|
+
self.stream_id,
|
|
136
|
+
self.endpoint,
|
|
137
|
+
already_locked=already_locked,
|
|
138
|
+
)
|
|
139
|
+
await self.abort()
|
|
140
|
+
return
|
|
141
|
+
await self._finish_if_complete()
|
|
142
|
+
|
|
143
|
+
async def abort(self) -> None:
|
|
144
|
+
if self.closed:
|
|
145
|
+
return
|
|
146
|
+
self.closed = True
|
|
147
|
+
current = asyncio.current_task()
|
|
148
|
+
if self.relay_task is not None and self.relay_task is not current:
|
|
149
|
+
self.relay_task.cancel()
|
|
150
|
+
with suppress(asyncio.CancelledError):
|
|
151
|
+
await self.relay_task
|
|
152
|
+
self.session.connect_tunnels.pop(self.stream_id, None)
|
|
153
|
+
lease = self.session.stream_work_leases.pop(self.stream_id, None)
|
|
154
|
+
if lease is not None:
|
|
155
|
+
lease.release()
|
|
156
|
+
elif self.work_lease is not None:
|
|
157
|
+
self.work_lease.release()
|
|
158
|
+
await close_tcp_writer(self.upstream_writer)
|
|
159
|
+
|
|
160
|
+
async def _relay_upstream_to_client(self) -> None:
|
|
161
|
+
reset_stream = False
|
|
162
|
+
try:
|
|
163
|
+
while True:
|
|
164
|
+
chunk = await asyncio.wait_for(self.upstream_reader.read(65536), timeout=self.handler.config.http.idle_timeout)
|
|
165
|
+
if not chunk:
|
|
166
|
+
break
|
|
167
|
+
await self.handler._send_http3_tunnel_data(
|
|
168
|
+
self.session,
|
|
169
|
+
self.stream_id,
|
|
170
|
+
chunk,
|
|
171
|
+
end_stream=False,
|
|
172
|
+
endpoint=self.endpoint,
|
|
173
|
+
)
|
|
174
|
+
except asyncio.CancelledError:
|
|
175
|
+
raise
|
|
176
|
+
except Exception:
|
|
177
|
+
reset_stream = True
|
|
178
|
+
else:
|
|
179
|
+
with suppress(Exception):
|
|
180
|
+
await self.handler._send_http3_tunnel_data(
|
|
181
|
+
self.session,
|
|
182
|
+
self.stream_id,
|
|
183
|
+
b'',
|
|
184
|
+
end_stream=True,
|
|
185
|
+
endpoint=self.endpoint,
|
|
186
|
+
)
|
|
187
|
+
finally:
|
|
188
|
+
self.server_output_closed = True
|
|
189
|
+
if reset_stream:
|
|
190
|
+
with suppress(Exception):
|
|
191
|
+
await self.handler._reset_http3_tunnel_stream(self.session, self.stream_id, self.endpoint)
|
|
192
|
+
await self._finish_if_complete()
|
|
193
|
+
|
|
194
|
+
async def _finish_if_complete(self) -> None:
|
|
195
|
+
if self.client_input_closed and self.server_output_closed:
|
|
196
|
+
await self.abort()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class HTTP3DatagramHandler:
|
|
200
|
+
_EARLY_DATA_TICKET_SIZE = 4096
|
|
201
|
+
_WEBTRANSPORT_BIDI_STREAM_SIGNAL = 0x41
|
|
202
|
+
|
|
203
|
+
def __init__(self, *, app: ASGIApp, config: ServerConfig, listener: ListenerConfig, access_logger: AccessLogger, scheduler: ProductionScheduler | None = None, metrics: Metrics | None = None) -> None:
|
|
204
|
+
self.app = app
|
|
205
|
+
self.config = config
|
|
206
|
+
self.listener = listener
|
|
207
|
+
self.access_logger = access_logger
|
|
208
|
+
self.scheduler = scheduler
|
|
209
|
+
self.metrics = metrics
|
|
210
|
+
self.sessions: dict[tuple[str, int], HTTP3Session] = {}
|
|
211
|
+
self.sessions_by_local_cid: dict[bytes, HTTP3Session] = {}
|
|
212
|
+
self._lock = asyncio.Lock()
|
|
213
|
+
|
|
214
|
+
def _session_ticket_early_data_size(self, session: HTTP3Session) -> int:
|
|
215
|
+
if session.quic.handshake_driver is None:
|
|
216
|
+
return 0
|
|
217
|
+
if self.config.quic.early_data_policy == 'deny':
|
|
218
|
+
return 0
|
|
219
|
+
return self._EARLY_DATA_TICKET_SIZE
|
|
220
|
+
|
|
221
|
+
def _should_send_too_early(self, session: HTTP3Session) -> bool:
|
|
222
|
+
handshake = session.quic.handshake_driver
|
|
223
|
+
if handshake is None:
|
|
224
|
+
return False
|
|
225
|
+
if self.config.quic.early_data_policy != 'require':
|
|
226
|
+
return False
|
|
227
|
+
return bool(getattr(handshake, '_using_psk', False)) and not bool(getattr(handshake, 'early_data_accepted', False))
|
|
228
|
+
|
|
229
|
+
def _configure_session_handshake(self, session: HTTP3Session) -> None:
|
|
230
|
+
if not self.listener.ssl_enabled or session.quic.handshake_driver is not None:
|
|
231
|
+
return
|
|
232
|
+
context = build_server_ssl_context(self.listener)
|
|
233
|
+
assert context is not None
|
|
234
|
+
transport_parameters = TransportParameters(max_udp_payload_size=self.listener.max_datagram_size, max_streams_bidi=self.config.scheduler.max_streams or 128, max_streams_uni=self.config.scheduler.max_streams or 128, idle_timeout=int(self.config.quic.idle_timeout * 1000))
|
|
235
|
+
session.quic.configure_handshake(
|
|
236
|
+
QuicTlsHandshakeDriver(
|
|
237
|
+
is_client=False,
|
|
238
|
+
alpn=tuple(self.listener.alpn_protocols or ('h3',)),
|
|
239
|
+
server_name=self.listener.host or 'localhost',
|
|
240
|
+
certificate_pem=context.certificate_pem,
|
|
241
|
+
private_key_pem=context.private_key_pem,
|
|
242
|
+
private_key_password=context.private_key_password,
|
|
243
|
+
trusted_certificates=context.trusted_certificates,
|
|
244
|
+
require_client_certificate=context.require_client_certificate,
|
|
245
|
+
validation_policy=context.validation_policy,
|
|
246
|
+
cipher_suites=context.cipher_suites,
|
|
247
|
+
transport_parameters=transport_parameters,
|
|
248
|
+
enable_early_data=self.config.quic.early_data_policy != 'deny',
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def _queue_or_send(self, session: HTTP3Session, raw: bytes, endpoint: UDPEndpoint, addr: tuple[str, int]) -> None:
|
|
253
|
+
transport = getattr(endpoint, 'transport', None)
|
|
254
|
+
if transport is not None and transport.is_closing():
|
|
255
|
+
return
|
|
256
|
+
if self._can_send_now(session, raw):
|
|
257
|
+
endpoint.send(raw, addr)
|
|
258
|
+
session.bytes_sent += len(raw)
|
|
259
|
+
if self.metrics is not None:
|
|
260
|
+
self.metrics.quic_datagram_sent(len(raw))
|
|
261
|
+
return
|
|
262
|
+
session.quic.defer_datagram(raw)
|
|
263
|
+
session.pending_outbound.append(raw)
|
|
264
|
+
|
|
265
|
+
def _sync_quic_loss_metrics(self, session: HTTP3Session) -> None:
|
|
266
|
+
if self.metrics is None:
|
|
267
|
+
return
|
|
268
|
+
lost_total = int(getattr(session.quic, 'packets_lost_total', 0))
|
|
269
|
+
if lost_total > session.last_quic_packets_lost_total:
|
|
270
|
+
self.metrics.quic_packets_lost_observed(lost_total - session.last_quic_packets_lost_total)
|
|
271
|
+
session.last_quic_packets_lost_total = lost_total
|
|
272
|
+
pto_total = int(getattr(session.quic, 'pto_expirations_total', 0))
|
|
273
|
+
while pto_total > session.last_quic_pto_expirations_total:
|
|
274
|
+
self.metrics.quic_pto_expired()
|
|
275
|
+
session.last_quic_pto_expirations_total += 1
|
|
276
|
+
|
|
277
|
+
def _flush_pending_outbound(self, session: HTTP3Session, endpoint: UDPEndpoint) -> None:
|
|
278
|
+
if not session.pending_outbound:
|
|
279
|
+
return
|
|
280
|
+
transport = getattr(endpoint, 'transport', None)
|
|
281
|
+
if transport is not None and transport.is_closing():
|
|
282
|
+
return
|
|
283
|
+
remaining: list[bytes] = []
|
|
284
|
+
for raw in session.pending_outbound:
|
|
285
|
+
if self._can_send_now(session, raw):
|
|
286
|
+
session.quic.confirm_datagram_sent(raw)
|
|
287
|
+
endpoint.send(raw, session.addr)
|
|
288
|
+
session.bytes_sent += len(raw)
|
|
289
|
+
if self.metrics is not None:
|
|
290
|
+
self.metrics.quic_datagram_sent(len(raw))
|
|
291
|
+
else:
|
|
292
|
+
remaining.append(raw)
|
|
293
|
+
session.pending_outbound = remaining
|
|
294
|
+
|
|
295
|
+
def _can_send_now(self, session: HTTP3Session, raw: bytes) -> bool:
|
|
296
|
+
amplification_ok = session.address_validated or (session.bytes_sent + len(raw) <= (session.bytes_received * 3))
|
|
297
|
+
return amplification_ok and session.quic.can_transmit_datagram(raw)
|
|
298
|
+
|
|
299
|
+
def _cancel_session_timer(self, session: HTTP3Session) -> None:
|
|
300
|
+
if session.timer_handle is not None:
|
|
301
|
+
session.timer_handle.cancel()
|
|
302
|
+
session.timer_handle = None
|
|
303
|
+
|
|
304
|
+
def _next_session_delay(self, session: HTTP3Session) -> float | None:
|
|
305
|
+
delays: list[float] = []
|
|
306
|
+
runtime_delay = session.quic.next_runtime_deadline()
|
|
307
|
+
if runtime_delay is not None:
|
|
308
|
+
delays.append(runtime_delay)
|
|
309
|
+
for raw in session.pending_outbound:
|
|
310
|
+
delay = session.quic.next_transmit_delay(raw)
|
|
311
|
+
if delay is not None:
|
|
312
|
+
delays.append(delay)
|
|
313
|
+
if not delays:
|
|
314
|
+
return None
|
|
315
|
+
return max(0.0, min(delays))
|
|
316
|
+
|
|
317
|
+
def _arm_session_timer(self, session: HTTP3Session, endpoint: UDPEndpoint) -> None:
|
|
318
|
+
self._cancel_session_timer(session)
|
|
319
|
+
delay = self._next_session_delay(session)
|
|
320
|
+
if delay is None:
|
|
321
|
+
return
|
|
322
|
+
loop = asyncio.get_running_loop()
|
|
323
|
+
session.timer_handle = loop.call_later(delay, self._fire_session_timer, session, endpoint)
|
|
324
|
+
|
|
325
|
+
def _close_session(self, session: HTTP3Session) -> None:
|
|
326
|
+
removed = self.sessions.pop(session.addr, None)
|
|
327
|
+
if removed is session:
|
|
328
|
+
self.sessions_by_local_cid.pop(session.quic.local_cid, None)
|
|
329
|
+
if self.metrics is not None:
|
|
330
|
+
self.metrics.quic_session_closed()
|
|
331
|
+
|
|
332
|
+
async def close(self) -> None:
|
|
333
|
+
async with self._lock:
|
|
334
|
+
for session in list(self.sessions.values()):
|
|
335
|
+
self._cancel_session_timer(session)
|
|
336
|
+
await self._abort_session_tunnels(session)
|
|
337
|
+
await self._abort_session_websockets(session)
|
|
338
|
+
await self._abort_session_webtransports(session)
|
|
339
|
+
self._close_session(session)
|
|
340
|
+
self.sessions.clear()
|
|
341
|
+
self.sessions_by_local_cid.clear()
|
|
342
|
+
|
|
343
|
+
def _fire_session_timer(self, session: HTTP3Session, endpoint: UDPEndpoint) -> None:
|
|
344
|
+
transport = getattr(endpoint, 'transport', None)
|
|
345
|
+
if transport is None or transport.is_closing():
|
|
346
|
+
return
|
|
347
|
+
try:
|
|
348
|
+
loop = asyncio.get_running_loop()
|
|
349
|
+
except RuntimeError:
|
|
350
|
+
return
|
|
351
|
+
if loop.is_closed():
|
|
352
|
+
return
|
|
353
|
+
loop.create_task(self._on_session_timer(session, endpoint))
|
|
354
|
+
|
|
355
|
+
async def _on_session_timer(self, session: HTTP3Session, endpoint: UDPEndpoint) -> None:
|
|
356
|
+
async with self._lock:
|
|
357
|
+
session.timer_handle = None
|
|
358
|
+
transport = getattr(endpoint, 'transport', None)
|
|
359
|
+
if transport is None or transport.is_closing():
|
|
360
|
+
return
|
|
361
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
362
|
+
return
|
|
363
|
+
outbound = session.quic.drain_scheduled_datagrams()
|
|
364
|
+
for raw in outbound:
|
|
365
|
+
self._queue_or_send(session, raw, endpoint, session.addr)
|
|
366
|
+
self._flush_pending_outbound(session, endpoint)
|
|
367
|
+
self._arm_session_timer(session, endpoint)
|
|
368
|
+
|
|
369
|
+
async def handle_packet(self, packet: UDPPacket, endpoint: UDPEndpoint) -> None:
|
|
370
|
+
async with self._lock:
|
|
371
|
+
try:
|
|
372
|
+
parsed = decode_packet(packet.data, destination_connection_id_length=8)
|
|
373
|
+
except Exception:
|
|
374
|
+
return
|
|
375
|
+
if isinstance(parsed, QuicVersionNegotiationPacket):
|
|
376
|
+
return
|
|
377
|
+
if isinstance(parsed, QuicLongHeaderPacket):
|
|
378
|
+
dcid = parsed.destination_connection_id
|
|
379
|
+
scid = parsed.source_connection_id
|
|
380
|
+
elif isinstance(parsed, QuicShortHeaderPacket):
|
|
381
|
+
dcid = parsed.destination_connection_id
|
|
382
|
+
scid = b''
|
|
383
|
+
elif isinstance(parsed, QuicRetryPacket):
|
|
384
|
+
dcid = parsed.destination_connection_id
|
|
385
|
+
scid = parsed.source_connection_id
|
|
386
|
+
else:
|
|
387
|
+
return
|
|
388
|
+
session = self.sessions_by_local_cid.get(dcid)
|
|
389
|
+
allow_addr_fallback = not (
|
|
390
|
+
isinstance(parsed, QuicLongHeaderPacket)
|
|
391
|
+
and parsed.packet_type == QuicLongHeaderType.INITIAL
|
|
392
|
+
and not parsed.token
|
|
393
|
+
)
|
|
394
|
+
if session is None and allow_addr_fallback:
|
|
395
|
+
session = self.sessions.get(packet.addr)
|
|
396
|
+
if session is None and isinstance(parsed, QuicShortHeaderPacket):
|
|
397
|
+
for known_cid, known_session in self.sessions_by_local_cid.items():
|
|
398
|
+
try:
|
|
399
|
+
candidate = decode_packet(packet.data, destination_connection_id_length=len(known_cid))
|
|
400
|
+
except Exception:
|
|
401
|
+
continue
|
|
402
|
+
if isinstance(candidate, QuicShortHeaderPacket) and candidate.destination_connection_id == known_cid:
|
|
403
|
+
parsed = candidate
|
|
404
|
+
dcid = candidate.destination_connection_id
|
|
405
|
+
session = known_session
|
|
406
|
+
break
|
|
407
|
+
predecoded_events = None
|
|
408
|
+
if session is None:
|
|
409
|
+
if 'http3' in self.listener.enabled_protocols:
|
|
410
|
+
if not isinstance(parsed, QuicLongHeaderPacket) or parsed.packet_type != QuicLongHeaderType.INITIAL:
|
|
411
|
+
return
|
|
412
|
+
session = HTTP3Session(
|
|
413
|
+
addr=packet.addr,
|
|
414
|
+
quic=QuicConnection(
|
|
415
|
+
is_client=False,
|
|
416
|
+
secret=self.listener.quic_secret,
|
|
417
|
+
local_cid=dcid or b'tigrcorn',
|
|
418
|
+
remote_cid=scid,
|
|
419
|
+
require_retry=self.listener.quic_require_retry,
|
|
420
|
+
),
|
|
421
|
+
)
|
|
422
|
+
self._configure_session_handshake(session)
|
|
423
|
+
else:
|
|
424
|
+
candidate_session = None
|
|
425
|
+
for cid_length in range(1, 21):
|
|
426
|
+
try:
|
|
427
|
+
candidate_packet = decode_packet(packet.data, destination_connection_id_length=cid_length)
|
|
428
|
+
except Exception:
|
|
429
|
+
continue
|
|
430
|
+
if not isinstance(candidate_packet, QuicShortHeaderPacket):
|
|
431
|
+
continue
|
|
432
|
+
probe = HTTP3Session(
|
|
433
|
+
addr=packet.addr,
|
|
434
|
+
quic=QuicConnection(
|
|
435
|
+
is_client=False,
|
|
436
|
+
secret=self.listener.quic_secret,
|
|
437
|
+
local_cid=candidate_packet.destination_connection_id,
|
|
438
|
+
remote_cid=candidate_packet.destination_connection_id,
|
|
439
|
+
require_retry=self.listener.quic_require_retry,
|
|
440
|
+
),
|
|
441
|
+
)
|
|
442
|
+
try:
|
|
443
|
+
events = probe.quic.receive_datagram(packet.data, addr=packet.addr)
|
|
444
|
+
except Exception:
|
|
445
|
+
continue
|
|
446
|
+
if any(event.kind != 'integrity_error' for event in events):
|
|
447
|
+
candidate_session = probe
|
|
448
|
+
parsed = candidate_packet
|
|
449
|
+
predecoded_events = events
|
|
450
|
+
break
|
|
451
|
+
if candidate_session is None:
|
|
452
|
+
return
|
|
453
|
+
session = candidate_session
|
|
454
|
+
self._configure_session_handshake(session)
|
|
455
|
+
self.sessions[packet.addr] = session
|
|
456
|
+
if session.quic.local_cid:
|
|
457
|
+
self.sessions_by_local_cid[session.quic.local_cid] = session
|
|
458
|
+
if self.metrics is not None:
|
|
459
|
+
self.metrics.quic_session_opened()
|
|
460
|
+
else:
|
|
461
|
+
session.quic.remote_cid = scid or session.quic.remote_cid
|
|
462
|
+
|
|
463
|
+
outbound: list[bytes] = []
|
|
464
|
+
|
|
465
|
+
session.bytes_received += len(packet.data)
|
|
466
|
+
if self.metrics is not None:
|
|
467
|
+
self.metrics.quic_datagram_received(len(packet.data))
|
|
468
|
+
if predecoded_events is None:
|
|
469
|
+
try:
|
|
470
|
+
events = session.quic.receive_datagram(packet.data, addr=packet.addr)
|
|
471
|
+
except Exception:
|
|
472
|
+
return
|
|
473
|
+
else:
|
|
474
|
+
events = predecoded_events
|
|
475
|
+
if session.addr != packet.addr and not any(event.kind == 'close' for event in events):
|
|
476
|
+
self.sessions.pop(session.addr, None)
|
|
477
|
+
session.addr = packet.addr
|
|
478
|
+
session.address_validated = True
|
|
479
|
+
session.quic.address_validated = True
|
|
480
|
+
self.sessions[packet.addr] = session
|
|
481
|
+
if session.quic.local_cid:
|
|
482
|
+
self.sessions_by_local_cid[session.quic.local_cid] = session
|
|
483
|
+
session.request_packets += 1
|
|
484
|
+
outbound.extend(self._ensure_server_control_stream_locked(session))
|
|
485
|
+
for event in events:
|
|
486
|
+
if self.metrics is not None:
|
|
487
|
+
if event.kind == 'retry':
|
|
488
|
+
self.metrics.quic_retry_emitted()
|
|
489
|
+
elif event.kind == 'path_challenge':
|
|
490
|
+
self.metrics.quic_path_challenge_observed()
|
|
491
|
+
elif event.kind == 'path_response':
|
|
492
|
+
self.metrics.quic_path_response_observed()
|
|
493
|
+
elif event.kind == 'path_migrated':
|
|
494
|
+
self.metrics.quic_path_migrated()
|
|
495
|
+
elif event.kind == 'reset_stream':
|
|
496
|
+
self.metrics.http3_stream_reset()
|
|
497
|
+
if event.kind == 'handshake_complete':
|
|
498
|
+
session.address_validated = True
|
|
499
|
+
session.quic.address_validated = True
|
|
500
|
+
if self.metrics is not None:
|
|
501
|
+
self.metrics.tls_handshake_completed()
|
|
502
|
+
if not session.early_data_accounted and session.quic.handshake_driver is not None:
|
|
503
|
+
using_psk = bool(getattr(session.quic.handshake_driver, '_using_psk', False))
|
|
504
|
+
if using_psk:
|
|
505
|
+
accepted = bool(getattr(session.quic.handshake_driver, 'early_data_accepted', False))
|
|
506
|
+
self.metrics.quic_early_data_observed(accepted=accepted)
|
|
507
|
+
session.early_data_accounted = True
|
|
508
|
+
outbound.extend(session.quic.take_handshake_datagrams())
|
|
509
|
+
outbound.extend(self._ensure_server_control_stream_locked(session))
|
|
510
|
+
if (
|
|
511
|
+
session.quic.handshake_driver is not None
|
|
512
|
+
and not session.quic.is_client
|
|
513
|
+
and not session.session_ticket_issued
|
|
514
|
+
):
|
|
515
|
+
try:
|
|
516
|
+
ticket = session.quic.handshake_driver.issue_session_ticket(
|
|
517
|
+
max_early_data_size=self._session_ticket_early_data_size(session)
|
|
518
|
+
)
|
|
519
|
+
except Exception:
|
|
520
|
+
ticket = b''
|
|
521
|
+
if ticket:
|
|
522
|
+
outbound.append(session.quic.send_crypto_data(ticket, packet_space='application'))
|
|
523
|
+
session.session_ticket_issued = True
|
|
524
|
+
elif event.kind == 'path_response':
|
|
525
|
+
session.address_validated = True
|
|
526
|
+
session.quic.address_validated = True
|
|
527
|
+
outbound.extend(self._ensure_server_control_stream_locked(session))
|
|
528
|
+
elif event.kind == 'stream' and event.stream_id is not None:
|
|
529
|
+
if 'http3' in self.listener.enabled_protocols:
|
|
530
|
+
try:
|
|
531
|
+
handled, h3_payload = await self._consume_webtransport_stream_event_locked(
|
|
532
|
+
session,
|
|
533
|
+
event.stream_id,
|
|
534
|
+
event.data,
|
|
535
|
+
fin=event.fin,
|
|
536
|
+
)
|
|
537
|
+
except HTTP3ConnectionError as exc:
|
|
538
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
539
|
+
outbound.append(session.quic.close(error_code=exc.error_code, reason=str(exc), application=True))
|
|
540
|
+
await self._abort_session_tunnels(session)
|
|
541
|
+
await self._abort_session_websockets(session)
|
|
542
|
+
await self._abort_session_webtransports(session)
|
|
543
|
+
self._cancel_session_timer(session)
|
|
544
|
+
self._close_session(session)
|
|
545
|
+
break
|
|
546
|
+
if handled:
|
|
547
|
+
continue
|
|
548
|
+
try:
|
|
549
|
+
peer_goaway_before = session.h3.state.peer_goaway_id
|
|
550
|
+
request_state = session.h3.receive_stream_data(event.stream_id, h3_payload, fin=event.fin)
|
|
551
|
+
if (
|
|
552
|
+
self.metrics is not None
|
|
553
|
+
and session.h3.state.peer_goaway_id is not None
|
|
554
|
+
and session.h3.state.peer_goaway_id != peer_goaway_before
|
|
555
|
+
):
|
|
556
|
+
self.metrics.http3_goaway_observed()
|
|
557
|
+
except HTTP3StreamError as exc:
|
|
558
|
+
if exc.stream_id is not None:
|
|
559
|
+
session.h3.abandon_stream(exc.stream_id)
|
|
560
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
561
|
+
if exc.stream_id is not None:
|
|
562
|
+
outbound.append(session.quic.reset_stream(exc.stream_id, exc.error_code))
|
|
563
|
+
continue
|
|
564
|
+
except HTTP3ConnectionError as exc:
|
|
565
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
566
|
+
outbound.append(session.quic.close(error_code=exc.error_code, reason=str(exc), application=True))
|
|
567
|
+
await self._abort_session_tunnels(session)
|
|
568
|
+
await self._abort_session_websockets(session)
|
|
569
|
+
await self._abort_session_webtransports(session)
|
|
570
|
+
self._cancel_session_timer(session)
|
|
571
|
+
self._close_session(session)
|
|
572
|
+
break
|
|
573
|
+
except ProtocolError as exc:
|
|
574
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
575
|
+
outbound.append(session.quic.close(error_code=H3_GENERAL_PROTOCOL_ERROR, reason=str(exc), application=True))
|
|
576
|
+
await self._abort_session_tunnels(session)
|
|
577
|
+
await self._abort_session_websockets(session)
|
|
578
|
+
await self._abort_session_webtransports(session)
|
|
579
|
+
self._cancel_session_timer(session)
|
|
580
|
+
self._close_session(session)
|
|
581
|
+
break
|
|
582
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
583
|
+
if request_state is not None:
|
|
584
|
+
header_map: dict[bytes, bytes] | None = None
|
|
585
|
+
if request_state.received_initial_headers:
|
|
586
|
+
try:
|
|
587
|
+
header_map = self._validate_request_headers(list(request_state.headers))
|
|
588
|
+
except ProtocolError:
|
|
589
|
+
if event.stream_id not in session.responded_streams:
|
|
590
|
+
outbound.extend(
|
|
591
|
+
self._build_http3_response_datagrams_locked(
|
|
592
|
+
session,
|
|
593
|
+
event.stream_id,
|
|
594
|
+
400,
|
|
595
|
+
[(b'content-type', b'text/plain')],
|
|
596
|
+
b'bad request',
|
|
597
|
+
end_stream=True,
|
|
598
|
+
)
|
|
599
|
+
)
|
|
600
|
+
session.responded_streams.add(event.stream_id)
|
|
601
|
+
outbound.extend(await self._respond_ready_requests(session, endpoint))
|
|
602
|
+
continue
|
|
603
|
+
protocol = header_map.get(b':protocol') if header_map is not None else None
|
|
604
|
+
if header_map is not None and protocol is not None and event.stream_id not in session.responded_streams:
|
|
605
|
+
if protocol == b'webtransport' and 'webtransport' in self.listener.enabled_protocols:
|
|
606
|
+
outbound.extend(
|
|
607
|
+
await self._start_webtransport_stream_locked(
|
|
608
|
+
session,
|
|
609
|
+
event.stream_id,
|
|
610
|
+
request_state,
|
|
611
|
+
header_map,
|
|
612
|
+
endpoint,
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
elif protocol != b'websocket' or not self.listener.websocket:
|
|
616
|
+
target = self._request_target_from_header_map(header_map)
|
|
617
|
+
self.access_logger.log_http(session.addr, 'CONNECT', target, 501, 'HTTP/3')
|
|
618
|
+
outbound.extend(
|
|
619
|
+
self._build_http3_response_datagrams_locked(
|
|
620
|
+
session,
|
|
621
|
+
event.stream_id,
|
|
622
|
+
501,
|
|
623
|
+
[(b'content-type', b'text/plain')],
|
|
624
|
+
b'unsupported extended connect protocol',
|
|
625
|
+
end_stream=True,
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
outbound.extend(
|
|
630
|
+
await self._start_websocket_stream_locked(
|
|
631
|
+
session,
|
|
632
|
+
event.stream_id,
|
|
633
|
+
request_state,
|
|
634
|
+
header_map,
|
|
635
|
+
endpoint,
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
session.responded_streams.add(event.stream_id)
|
|
639
|
+
elif header_map is not None and header_map.get(b':method') == b'CONNECT' and event.stream_id not in session.responded_streams:
|
|
640
|
+
outbound.extend(
|
|
641
|
+
await self._start_connect_tunnel_locked(
|
|
642
|
+
session,
|
|
643
|
+
event.stream_id,
|
|
644
|
+
request_state,
|
|
645
|
+
header_map,
|
|
646
|
+
endpoint,
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
session.responded_streams.add(event.stream_id)
|
|
650
|
+
if event.stream_id in session.websocket_sessions:
|
|
651
|
+
await self._drain_websocket_request_body_locked(session, event.stream_id, request_state, endpoint)
|
|
652
|
+
elif event.stream_id in session.connect_tunnels:
|
|
653
|
+
await self._drain_connect_request_body_locked(session, event.stream_id, request_state)
|
|
654
|
+
elif event.stream_id in session.webtransport_streams:
|
|
655
|
+
await self._drain_webtransport_request_body_locked(session, event.stream_id, request_state)
|
|
656
|
+
elif request_state.ready and event.stream_id not in session.responded_streams:
|
|
657
|
+
outbound.extend(await self._invoke_http_app(session, event.stream_id, request_state, endpoint))
|
|
658
|
+
session.responded_streams.add(event.stream_id)
|
|
659
|
+
outbound.extend(await self._respond_ready_requests(session, endpoint))
|
|
660
|
+
else:
|
|
661
|
+
outbound.extend(await self._invoke_custom_quic_app(session, event, endpoint))
|
|
662
|
+
if event.stream_id is not None:
|
|
663
|
+
session.responded_streams.add(event.stream_id)
|
|
664
|
+
elif event.kind == 'reset_stream' and event.stream_id is not None:
|
|
665
|
+
if 'http3' in self.listener.enabled_protocols:
|
|
666
|
+
websocket = session.websocket_sessions.get(event.stream_id)
|
|
667
|
+
if websocket is not None:
|
|
668
|
+
await websocket.abort()
|
|
669
|
+
session.websocket_sessions.pop(event.stream_id, None)
|
|
670
|
+
tunnel = session.connect_tunnels.get(event.stream_id)
|
|
671
|
+
if tunnel is not None:
|
|
672
|
+
await tunnel.abort()
|
|
673
|
+
if event.stream_id in session.webtransport_streams:
|
|
674
|
+
owner_stream_id = session.webtransport_stream_owners.get(event.stream_id, event.stream_id)
|
|
675
|
+
webtransport = session.webtransport_sessions.get(owner_stream_id)
|
|
676
|
+
if owner_stream_id == event.stream_id and webtransport is not None:
|
|
677
|
+
webtransport.note_connect_stream_stopped()
|
|
678
|
+
else:
|
|
679
|
+
session.webtransport_streams.discard(event.stream_id)
|
|
680
|
+
session.webtransport_stream_prefaces.pop(event.stream_id, None)
|
|
681
|
+
session.webtransport_stream_owners.pop(event.stream_id, None)
|
|
682
|
+
webtransport = session.webtransport_sessions.pop(owner_stream_id, None) if owner_stream_id == event.stream_id else None
|
|
683
|
+
if webtransport is not None:
|
|
684
|
+
await webtransport.abort()
|
|
685
|
+
self._release_stream_work_lease(session, event.stream_id)
|
|
686
|
+
if event.stream_id not in session.webtransport_sessions:
|
|
687
|
+
session.h3.abandon_stream(event.stream_id)
|
|
688
|
+
outbound.extend(self._flush_qpack_streams(session))
|
|
689
|
+
elif event.kind == 'stop_sending' and event.stream_id is not None:
|
|
690
|
+
if 'http3' in self.listener.enabled_protocols and event.stream_id in session.webtransport_sessions:
|
|
691
|
+
session.quic.suppress_pending_reset(event.stream_id)
|
|
692
|
+
webtransport = session.webtransport_sessions.get(event.stream_id)
|
|
693
|
+
if webtransport is not None:
|
|
694
|
+
webtransport.note_connect_stream_stopped()
|
|
695
|
+
elif event.kind == 'datagram':
|
|
696
|
+
if 'http3' in self.listener.enabled_protocols:
|
|
697
|
+
await self._dispatch_webtransport_datagram_locked(session, event.data)
|
|
698
|
+
elif event.kind == 'close':
|
|
699
|
+
await self._abort_session_tunnels(session)
|
|
700
|
+
await self._abort_session_websockets(session)
|
|
701
|
+
await self._abort_session_webtransports(session)
|
|
702
|
+
self._cancel_session_timer(session)
|
|
703
|
+
self._close_session(session)
|
|
704
|
+
self._sync_quic_loss_metrics(session)
|
|
705
|
+
outbound.extend(session.quic.take_handshake_datagrams())
|
|
706
|
+
outbound.extend(session.quic.drain_scheduled_datagrams())
|
|
707
|
+
for raw in outbound:
|
|
708
|
+
self._queue_or_send(session, raw, endpoint, packet.addr)
|
|
709
|
+
self._flush_pending_outbound(session, endpoint)
|
|
710
|
+
if session.addr in self.sessions and self.sessions.get(session.addr) is session:
|
|
711
|
+
self._arm_session_timer(session, endpoint)
|
|
712
|
+
|
|
713
|
+
def _ensure_server_control_stream_locked(self, session: HTTP3Session) -> list[bytes]:
|
|
714
|
+
if (
|
|
715
|
+
session.server_control_stream_sent
|
|
716
|
+
or 'http3' not in self.listener.enabled_protocols
|
|
717
|
+
or (not session.address_validated and session.quic.handshake_driver is not None)
|
|
718
|
+
):
|
|
719
|
+
return []
|
|
720
|
+
if session.server_control_stream_id is None:
|
|
721
|
+
session.server_control_stream_id = session.quic.streams.next_stream_id(client=False, unidirectional=True)
|
|
722
|
+
control_settings = {1: 0, 6: self.listener.max_datagram_size}
|
|
723
|
+
if self.listener.websocket:
|
|
724
|
+
control_settings[SETTING_ENABLE_CONNECT_PROTOCOL] = 1
|
|
725
|
+
if 'webtransport' in self.listener.enabled_protocols:
|
|
726
|
+
control_settings[SETTING_ENABLE_CONNECT_PROTOCOL] = 1
|
|
727
|
+
control_settings[SETTING_H3_DATAGRAM] = 1
|
|
728
|
+
control_settings[SETTING_ENABLE_WEBTRANSPORT] = 1
|
|
729
|
+
control_payload = session.h3.encode_control_stream(control_settings)
|
|
730
|
+
session.server_control_stream_sent = True
|
|
731
|
+
return [session.quic.send_stream_data(session.server_control_stream_id, control_payload, fin=False)]
|
|
732
|
+
|
|
733
|
+
def _flush_qpack_streams(self, session: HTTP3Session) -> list[bytes]:
|
|
734
|
+
outbound: list[bytes] = []
|
|
735
|
+
encoder_data = session.h3.take_encoder_stream_data()
|
|
736
|
+
if encoder_data:
|
|
737
|
+
if session.server_qpack_encoder_stream_id is None:
|
|
738
|
+
session.server_qpack_encoder_stream_id = session.quic.streams.next_stream_id(client=False, unidirectional=True)
|
|
739
|
+
encoder_data = encode_quic_varint(STREAM_TYPE_QPACK_ENCODER) + encoder_data
|
|
740
|
+
if self.metrics is not None:
|
|
741
|
+
self.metrics.http3_qpack_encoder_stream_opened()
|
|
742
|
+
outbound.append(session.quic.send_stream_data(session.server_qpack_encoder_stream_id, encoder_data, fin=False))
|
|
743
|
+
decoder_data = session.h3.take_decoder_stream_data()
|
|
744
|
+
if decoder_data:
|
|
745
|
+
if session.server_qpack_decoder_stream_id is None:
|
|
746
|
+
session.server_qpack_decoder_stream_id = session.quic.streams.next_stream_id(client=False, unidirectional=True)
|
|
747
|
+
decoder_data = encode_quic_varint(STREAM_TYPE_QPACK_DECODER) + decoder_data
|
|
748
|
+
if self.metrics is not None:
|
|
749
|
+
self.metrics.http3_qpack_decoder_stream_opened()
|
|
750
|
+
outbound.append(session.quic.send_stream_data(session.server_qpack_decoder_stream_id, decoder_data, fin=False))
|
|
751
|
+
return outbound
|
|
752
|
+
|
|
753
|
+
def _queue_session_outbound_locked(self, session: HTTP3Session, outbound: list[bytes], endpoint: UDPEndpoint) -> None:
|
|
754
|
+
for raw in outbound:
|
|
755
|
+
self._queue_or_send(session, raw, endpoint, session.addr)
|
|
756
|
+
self._flush_pending_outbound(session, endpoint)
|
|
757
|
+
if session.addr in self.sessions and self.sessions.get(session.addr) is session:
|
|
758
|
+
self._arm_session_timer(session, endpoint)
|
|
759
|
+
|
|
760
|
+
def _webtransport_max_datagram_size(self) -> int:
|
|
761
|
+
# Keep the public config path discoverable for SSOT proof checks: webtransport.max_datagram_size.
|
|
762
|
+
configured = self.config.webtransport.max_datagram_size
|
|
763
|
+
return int(configured if configured is not None else self.listener.max_datagram_size)
|
|
764
|
+
|
|
765
|
+
def _webtransport_security_extension(self, session: HTTP3Session) -> dict[str, object]:
|
|
766
|
+
handshake = session.quic.handshake_driver
|
|
767
|
+
peer_certificate = getattr(handshake, 'peer_certificate_pem', None)
|
|
768
|
+
return {
|
|
769
|
+
'alpn': getattr(handshake, 'selected_alpn', None),
|
|
770
|
+
'mtls': bool(peer_certificate),
|
|
771
|
+
'peer_certificate': peer_certificate,
|
|
772
|
+
'sni': getattr(handshake, 'server_name', None),
|
|
773
|
+
'tls': bool(self.listener.ssl_enabled and handshake is not None and getattr(handshake, 'complete', False)),
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
def _webtransport_transport_extension(self, session: HTTP3Session) -> dict[str, object]:
|
|
777
|
+
return {
|
|
778
|
+
'address_validated': bool(session.address_validated or session.quic.address_validated),
|
|
779
|
+
'connection_id': session.quic.local_cid.hex(),
|
|
780
|
+
'max_datagram_size': self._webtransport_max_datagram_size(),
|
|
781
|
+
'retry_required': bool(self.listener.quic_require_retry),
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
def _encode_webtransport_datagram_payload(self, stream_id: int, data: bytes) -> bytes:
|
|
785
|
+
if len(data) > self._webtransport_max_datagram_size():
|
|
786
|
+
raise ProtocolError('webtransport.max_datagram_size exceeded')
|
|
787
|
+
quarter_stream_id = stream_id // 4
|
|
788
|
+
return encode_quic_varint(quarter_stream_id) + data
|
|
789
|
+
|
|
790
|
+
def _decode_webtransport_datagram_payload(self, payload: bytes) -> tuple[int, bytes]:
|
|
791
|
+
quarter_stream_id, offset = decode_quic_varint(payload, 0)
|
|
792
|
+
return int(quarter_stream_id) * 4, payload[offset:]
|
|
793
|
+
|
|
794
|
+
def _stream_is_client_initiated_bidi(self, stream_id: int) -> bool:
|
|
795
|
+
return (stream_id & 0x03) == 0x00
|
|
796
|
+
|
|
797
|
+
def _parse_webtransport_bidi_stream_prefix(self, payload: bytes) -> tuple[int, int, bytes] | None:
|
|
798
|
+
try:
|
|
799
|
+
signal, offset = decode_quic_varint(payload, 0)
|
|
800
|
+
except ValueError:
|
|
801
|
+
return None
|
|
802
|
+
if signal != self._WEBTRANSPORT_BIDI_STREAM_SIGNAL:
|
|
803
|
+
return (-1, -1, payload)
|
|
804
|
+
try:
|
|
805
|
+
session_id, offset = decode_quic_varint(payload, offset)
|
|
806
|
+
except ValueError:
|
|
807
|
+
return None
|
|
808
|
+
return signal, session_id, payload[offset:]
|
|
809
|
+
|
|
810
|
+
async def _consume_webtransport_stream_event_locked(
|
|
811
|
+
self,
|
|
812
|
+
session: HTTP3Session,
|
|
813
|
+
stream_id: int,
|
|
814
|
+
data: bytes,
|
|
815
|
+
*,
|
|
816
|
+
fin: bool,
|
|
817
|
+
) -> tuple[bool, bytes]:
|
|
818
|
+
owner_stream_id = session.webtransport_stream_owners.get(stream_id)
|
|
819
|
+
if owner_stream_id is not None and owner_stream_id != stream_id:
|
|
820
|
+
webtransport = session.webtransport_sessions.get(owner_stream_id)
|
|
821
|
+
if webtransport is not None:
|
|
822
|
+
await webtransport.feed_stream_data(
|
|
823
|
+
data,
|
|
824
|
+
end_stream=fin,
|
|
825
|
+
disconnect_on_end=False,
|
|
826
|
+
stream_id=stream_id,
|
|
827
|
+
)
|
|
828
|
+
return True, b''
|
|
829
|
+
if owner_stream_id == stream_id:
|
|
830
|
+
return False, data
|
|
831
|
+
if not self._stream_is_client_initiated_bidi(stream_id):
|
|
832
|
+
return False, data
|
|
833
|
+
if stream_id in session.h3.requests:
|
|
834
|
+
return False, data
|
|
835
|
+
|
|
836
|
+
preface = session.webtransport_stream_prefaces.setdefault(stream_id, bytearray())
|
|
837
|
+
preface.extend(data)
|
|
838
|
+
parsed = self._parse_webtransport_bidi_stream_prefix(bytes(preface))
|
|
839
|
+
if parsed is None:
|
|
840
|
+
if fin:
|
|
841
|
+
payload = bytes(preface)
|
|
842
|
+
session.webtransport_stream_prefaces.pop(stream_id, None)
|
|
843
|
+
return False, payload
|
|
844
|
+
return True, b''
|
|
845
|
+
|
|
846
|
+
signal, owner_candidate, remaining = parsed
|
|
847
|
+
if signal != self._WEBTRANSPORT_BIDI_STREAM_SIGNAL:
|
|
848
|
+
session.webtransport_stream_prefaces.pop(stream_id, None)
|
|
849
|
+
return False, bytes(preface)
|
|
850
|
+
|
|
851
|
+
webtransport = session.webtransport_sessions.get(owner_candidate)
|
|
852
|
+
if webtransport is None:
|
|
853
|
+
raise HTTP3ConnectionError(
|
|
854
|
+
f'invalid WebTransport session id {owner_candidate} on stream {stream_id}',
|
|
855
|
+
error_code=H3_ID_ERROR,
|
|
856
|
+
)
|
|
857
|
+
session.webtransport_streams.add(stream_id)
|
|
858
|
+
session.webtransport_stream_owners[stream_id] = owner_candidate
|
|
859
|
+
session.webtransport_stream_prefaces.pop(stream_id, None)
|
|
860
|
+
await webtransport.feed_stream_data(
|
|
861
|
+
remaining,
|
|
862
|
+
end_stream=fin,
|
|
863
|
+
disconnect_on_end=False,
|
|
864
|
+
stream_id=stream_id,
|
|
865
|
+
)
|
|
866
|
+
return True, b''
|
|
867
|
+
|
|
868
|
+
async def _dispatch_webtransport_datagram_locked(self, session: HTTP3Session, payload: bytes) -> None:
|
|
869
|
+
try:
|
|
870
|
+
stream_id, data = self._decode_webtransport_datagram_payload(payload)
|
|
871
|
+
except ProtocolError:
|
|
872
|
+
return
|
|
873
|
+
webtransport = session.webtransport_sessions.get(stream_id)
|
|
874
|
+
if webtransport is None and len(session.webtransport_sessions) == 1:
|
|
875
|
+
webtransport = next(iter(session.webtransport_sessions.values()))
|
|
876
|
+
if webtransport is None:
|
|
877
|
+
return
|
|
878
|
+
if len(data) > self._webtransport_max_datagram_size():
|
|
879
|
+
return
|
|
880
|
+
datagram_id = f'{stream_id}:{getattr(session, "request_packets", 0)}'
|
|
881
|
+
await webtransport.feed_datagram(datagram_id, data)
|
|
882
|
+
|
|
883
|
+
async def _send_webtransport_stream_data(
|
|
884
|
+
self,
|
|
885
|
+
session: HTTP3Session,
|
|
886
|
+
stream_id: int,
|
|
887
|
+
data: bytes,
|
|
888
|
+
*,
|
|
889
|
+
end_stream: bool,
|
|
890
|
+
endpoint: UDPEndpoint,
|
|
891
|
+
already_locked: bool = False,
|
|
892
|
+
) -> None:
|
|
893
|
+
if not already_locked:
|
|
894
|
+
async with self._lock:
|
|
895
|
+
await self._send_webtransport_stream_data(
|
|
896
|
+
session,
|
|
897
|
+
stream_id,
|
|
898
|
+
data,
|
|
899
|
+
end_stream=end_stream,
|
|
900
|
+
endpoint=endpoint,
|
|
901
|
+
already_locked=True,
|
|
902
|
+
)
|
|
903
|
+
return
|
|
904
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
905
|
+
return
|
|
906
|
+
owner_stream_id = session.webtransport_stream_owners.get(stream_id)
|
|
907
|
+
if owner_stream_id is None:
|
|
908
|
+
return
|
|
909
|
+
if owner_stream_id == stream_id:
|
|
910
|
+
outbound = self._build_http3_data_datagrams_locked(session, stream_id, data, end_stream=end_stream)
|
|
911
|
+
else:
|
|
912
|
+
outbound = [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, data, fin=end_stream)]
|
|
913
|
+
if end_stream:
|
|
914
|
+
session.webtransport_streams.discard(stream_id)
|
|
915
|
+
session.webtransport_stream_owners.pop(stream_id, None)
|
|
916
|
+
session.webtransport_stream_prefaces.pop(stream_id, None)
|
|
917
|
+
if owner_stream_id == stream_id:
|
|
918
|
+
session.webtransport_sessions.pop(stream_id, None)
|
|
919
|
+
self._release_stream_work_lease(session, stream_id)
|
|
920
|
+
session.h3.abandon_stream(stream_id)
|
|
921
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
922
|
+
|
|
923
|
+
async def _send_webtransport_datagram(
|
|
924
|
+
self,
|
|
925
|
+
session: HTTP3Session,
|
|
926
|
+
stream_id: int,
|
|
927
|
+
data: bytes,
|
|
928
|
+
*,
|
|
929
|
+
datagram_id: str,
|
|
930
|
+
endpoint: UDPEndpoint,
|
|
931
|
+
already_locked: bool = False,
|
|
932
|
+
) -> None:
|
|
933
|
+
if not already_locked:
|
|
934
|
+
async with self._lock:
|
|
935
|
+
await self._send_webtransport_datagram(
|
|
936
|
+
session,
|
|
937
|
+
stream_id,
|
|
938
|
+
data,
|
|
939
|
+
datagram_id=datagram_id,
|
|
940
|
+
endpoint=endpoint,
|
|
941
|
+
already_locked=True,
|
|
942
|
+
)
|
|
943
|
+
return
|
|
944
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
945
|
+
return
|
|
946
|
+
if stream_id not in session.webtransport_sessions:
|
|
947
|
+
return
|
|
948
|
+
payload = self._encode_webtransport_datagram_payload(stream_id, data)
|
|
949
|
+
outbound = [session.quic.send_datagram_frame(payload)]
|
|
950
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
951
|
+
|
|
952
|
+
def _build_http3_response_datagrams_locked(
|
|
953
|
+
self,
|
|
954
|
+
session: HTTP3Session,
|
|
955
|
+
stream_id: int,
|
|
956
|
+
status: int,
|
|
957
|
+
headers: list[tuple[bytes, bytes]],
|
|
958
|
+
body: bytes,
|
|
959
|
+
*,
|
|
960
|
+
end_stream: bool,
|
|
961
|
+
) -> list[bytes]:
|
|
962
|
+
response_headers = apply_response_header_policy(
|
|
963
|
+
strip_connection_specific_headers(headers),
|
|
964
|
+
server_header=self.config.server_header_value,
|
|
965
|
+
include_date_header=self.config.include_date_header,
|
|
966
|
+
default_headers=self.config.default_response_headers,
|
|
967
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version='3'),
|
|
968
|
+
)
|
|
969
|
+
header_block = session.h3.encode_headers(
|
|
970
|
+
stream_id,
|
|
971
|
+
[(b':status', str(status).encode('ascii')), *response_headers],
|
|
972
|
+
)
|
|
973
|
+
payload = bytearray(encode_frame(FRAME_HEADERS, header_block))
|
|
974
|
+
if body:
|
|
975
|
+
payload.extend(encode_frame(FRAME_DATA, body))
|
|
976
|
+
return [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, bytes(payload), fin=end_stream)]
|
|
977
|
+
|
|
978
|
+
async def _send_http3_streamed_response_locked(
|
|
979
|
+
self,
|
|
980
|
+
session: HTTP3Session,
|
|
981
|
+
stream_id: int,
|
|
982
|
+
status: int,
|
|
983
|
+
headers: list[tuple[bytes, bytes]],
|
|
984
|
+
body_segments: list,
|
|
985
|
+
trailers: list[tuple[bytes, bytes]],
|
|
986
|
+
informational: list[tuple[int, list[tuple[bytes, bytes]]]],
|
|
987
|
+
endpoint: UDPEndpoint,
|
|
988
|
+
) -> None:
|
|
989
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
990
|
+
return
|
|
991
|
+
for interim_status, interim_headers in informational:
|
|
992
|
+
interim_header_block = session.h3.encode_headers(
|
|
993
|
+
stream_id,
|
|
994
|
+
[(b':status', str(interim_status).encode('ascii')), *sanitize_early_hints_headers(interim_headers)],
|
|
995
|
+
)
|
|
996
|
+
outbound = [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, encode_frame(FRAME_HEADERS, interim_header_block), fin=False)]
|
|
997
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
998
|
+
has_body = response_body_segments_have_bytes(body_segments)
|
|
999
|
+
response_headers = apply_response_header_policy(
|
|
1000
|
+
strip_connection_specific_headers(headers),
|
|
1001
|
+
server_header=self.config.server_header_value,
|
|
1002
|
+
include_date_header=self.config.include_date_header,
|
|
1003
|
+
default_headers=self.config.default_response_headers,
|
|
1004
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version='3'),
|
|
1005
|
+
)
|
|
1006
|
+
header_block = session.h3.encode_headers(stream_id, [(b':status', str(status).encode('ascii')), *response_headers])
|
|
1007
|
+
outbound = [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, encode_frame(FRAME_HEADERS, header_block), fin=(not has_body and not trailers))]
|
|
1008
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1009
|
+
if not has_body and not trailers:
|
|
1010
|
+
return
|
|
1011
|
+
if has_body:
|
|
1012
|
+
chunk_size = max(1024, int(self.listener.max_datagram_size) - 256)
|
|
1013
|
+
async for chunk in iter_response_body_segments(body_segments, chunk_size=chunk_size):
|
|
1014
|
+
outbound = self._build_http3_data_datagrams_locked(session, stream_id, chunk, end_stream=False)
|
|
1015
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1016
|
+
if trailers:
|
|
1017
|
+
trailer_block = session.h3.encode_headers(stream_id, list(trailers))
|
|
1018
|
+
outbound = [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, encode_frame(FRAME_HEADERS, trailer_block), fin=True)]
|
|
1019
|
+
else:
|
|
1020
|
+
outbound = self._build_http3_data_datagrams_locked(session, stream_id, b'', end_stream=True)
|
|
1021
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1022
|
+
|
|
1023
|
+
def _build_http3_data_datagrams_locked(
|
|
1024
|
+
self,
|
|
1025
|
+
session: HTTP3Session,
|
|
1026
|
+
stream_id: int,
|
|
1027
|
+
data: bytes,
|
|
1028
|
+
*,
|
|
1029
|
+
end_stream: bool,
|
|
1030
|
+
) -> list[bytes]:
|
|
1031
|
+
payload = encode_frame(FRAME_DATA, data) if data else b''
|
|
1032
|
+
return [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, payload, fin=end_stream)]
|
|
1033
|
+
|
|
1034
|
+
async def _send_http3_websocket_headers(
|
|
1035
|
+
self,
|
|
1036
|
+
session: HTTP3Session,
|
|
1037
|
+
stream_id: int,
|
|
1038
|
+
status: int,
|
|
1039
|
+
headers: list[tuple[bytes, bytes]],
|
|
1040
|
+
*,
|
|
1041
|
+
end_stream: bool,
|
|
1042
|
+
endpoint: UDPEndpoint,
|
|
1043
|
+
already_locked: bool = False,
|
|
1044
|
+
) -> None:
|
|
1045
|
+
if not already_locked:
|
|
1046
|
+
async with self._lock:
|
|
1047
|
+
await self._send_http3_websocket_headers(
|
|
1048
|
+
session,
|
|
1049
|
+
stream_id,
|
|
1050
|
+
status,
|
|
1051
|
+
headers,
|
|
1052
|
+
end_stream=end_stream,
|
|
1053
|
+
endpoint=endpoint,
|
|
1054
|
+
already_locked=True,
|
|
1055
|
+
)
|
|
1056
|
+
return
|
|
1057
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
1058
|
+
return
|
|
1059
|
+
if stream_id not in session.websocket_sessions:
|
|
1060
|
+
return
|
|
1061
|
+
outbound = self._build_http3_response_datagrams_locked(
|
|
1062
|
+
session,
|
|
1063
|
+
stream_id,
|
|
1064
|
+
status,
|
|
1065
|
+
headers,
|
|
1066
|
+
b'',
|
|
1067
|
+
end_stream=end_stream,
|
|
1068
|
+
)
|
|
1069
|
+
if end_stream:
|
|
1070
|
+
session.websocket_sessions.pop(stream_id, None)
|
|
1071
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1072
|
+
session.h3.abandon_stream(stream_id)
|
|
1073
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1074
|
+
|
|
1075
|
+
async def _send_http3_websocket_data(
|
|
1076
|
+
self,
|
|
1077
|
+
session: HTTP3Session,
|
|
1078
|
+
stream_id: int,
|
|
1079
|
+
data: bytes,
|
|
1080
|
+
*,
|
|
1081
|
+
end_stream: bool,
|
|
1082
|
+
endpoint: UDPEndpoint,
|
|
1083
|
+
already_locked: bool = False,
|
|
1084
|
+
) -> None:
|
|
1085
|
+
if not already_locked:
|
|
1086
|
+
async with self._lock:
|
|
1087
|
+
await self._send_http3_websocket_data(
|
|
1088
|
+
session,
|
|
1089
|
+
stream_id,
|
|
1090
|
+
data,
|
|
1091
|
+
end_stream=end_stream,
|
|
1092
|
+
endpoint=endpoint,
|
|
1093
|
+
already_locked=True,
|
|
1094
|
+
)
|
|
1095
|
+
return
|
|
1096
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
1097
|
+
return
|
|
1098
|
+
if stream_id not in session.websocket_sessions:
|
|
1099
|
+
return
|
|
1100
|
+
outbound = self._build_http3_data_datagrams_locked(session, stream_id, data, end_stream=end_stream)
|
|
1101
|
+
if end_stream:
|
|
1102
|
+
session.websocket_sessions.pop(stream_id, None)
|
|
1103
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1104
|
+
session.h3.abandon_stream(stream_id)
|
|
1105
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1106
|
+
|
|
1107
|
+
async def _send_http3_tunnel_data(
|
|
1108
|
+
self,
|
|
1109
|
+
session: HTTP3Session,
|
|
1110
|
+
stream_id: int,
|
|
1111
|
+
data: bytes,
|
|
1112
|
+
*,
|
|
1113
|
+
end_stream: bool,
|
|
1114
|
+
endpoint: UDPEndpoint,
|
|
1115
|
+
already_locked: bool = False,
|
|
1116
|
+
) -> None:
|
|
1117
|
+
if not already_locked:
|
|
1118
|
+
async with self._lock:
|
|
1119
|
+
await self._send_http3_tunnel_data(
|
|
1120
|
+
session,
|
|
1121
|
+
stream_id,
|
|
1122
|
+
data,
|
|
1123
|
+
end_stream=end_stream,
|
|
1124
|
+
endpoint=endpoint,
|
|
1125
|
+
already_locked=True,
|
|
1126
|
+
)
|
|
1127
|
+
return
|
|
1128
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
1129
|
+
return
|
|
1130
|
+
if stream_id not in session.connect_tunnels:
|
|
1131
|
+
return
|
|
1132
|
+
outbound = self._build_http3_data_datagrams_locked(session, stream_id, data, end_stream=end_stream)
|
|
1133
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1134
|
+
|
|
1135
|
+
async def _reset_http3_tunnel_stream(
|
|
1136
|
+
self,
|
|
1137
|
+
session: HTTP3Session,
|
|
1138
|
+
stream_id: int,
|
|
1139
|
+
endpoint: UDPEndpoint,
|
|
1140
|
+
*,
|
|
1141
|
+
already_locked: bool = False,
|
|
1142
|
+
) -> None:
|
|
1143
|
+
if not already_locked:
|
|
1144
|
+
async with self._lock:
|
|
1145
|
+
await self._reset_http3_tunnel_stream(
|
|
1146
|
+
session,
|
|
1147
|
+
stream_id,
|
|
1148
|
+
endpoint,
|
|
1149
|
+
already_locked=True,
|
|
1150
|
+
)
|
|
1151
|
+
return
|
|
1152
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
1153
|
+
return
|
|
1154
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1155
|
+
session.h3.abandon_stream(stream_id)
|
|
1156
|
+
outbound = self._flush_qpack_streams(session)
|
|
1157
|
+
outbound.append(session.quic.reset_stream(stream_id, H3_CONNECT_ERROR))
|
|
1158
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1159
|
+
|
|
1160
|
+
async def _abort_session_tunnels(self, session: HTTP3Session) -> None:
|
|
1161
|
+
for tunnel in list(session.connect_tunnels.values()):
|
|
1162
|
+
with suppress(Exception):
|
|
1163
|
+
await tunnel.abort()
|
|
1164
|
+
|
|
1165
|
+
async def _reset_http3_websocket_stream(
|
|
1166
|
+
self,
|
|
1167
|
+
session: HTTP3Session,
|
|
1168
|
+
stream_id: int,
|
|
1169
|
+
endpoint: UDPEndpoint,
|
|
1170
|
+
*,
|
|
1171
|
+
already_locked: bool = False,
|
|
1172
|
+
) -> None:
|
|
1173
|
+
if not already_locked:
|
|
1174
|
+
async with self._lock:
|
|
1175
|
+
await self._reset_http3_websocket_stream(
|
|
1176
|
+
session,
|
|
1177
|
+
stream_id,
|
|
1178
|
+
endpoint,
|
|
1179
|
+
already_locked=True,
|
|
1180
|
+
)
|
|
1181
|
+
return
|
|
1182
|
+
if session.addr not in self.sessions or self.sessions.get(session.addr) is not session:
|
|
1183
|
+
return
|
|
1184
|
+
session.websocket_sessions.pop(stream_id, None)
|
|
1185
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1186
|
+
session.h3.abandon_stream(stream_id)
|
|
1187
|
+
outbound = self._flush_qpack_streams(session)
|
|
1188
|
+
outbound.append(session.quic.reset_stream(stream_id, H3_REQUEST_CANCELLED))
|
|
1189
|
+
self._queue_session_outbound_locked(session, outbound, endpoint)
|
|
1190
|
+
|
|
1191
|
+
async def _abort_session_websockets(self, session: HTTP3Session) -> None:
|
|
1192
|
+
for websocket in list(session.websocket_sessions.values()):
|
|
1193
|
+
with suppress(Exception):
|
|
1194
|
+
await websocket.abort()
|
|
1195
|
+
session.websocket_sessions.clear()
|
|
1196
|
+
|
|
1197
|
+
async def _abort_session_webtransports(self, session: HTTP3Session) -> None:
|
|
1198
|
+
for webtransport in list(session.webtransport_sessions.values()):
|
|
1199
|
+
with suppress(Exception):
|
|
1200
|
+
await webtransport.abort()
|
|
1201
|
+
session.webtransport_sessions.clear()
|
|
1202
|
+
session.webtransport_streams.clear()
|
|
1203
|
+
session.webtransport_stream_owners.clear()
|
|
1204
|
+
session.webtransport_stream_prefaces.clear()
|
|
1205
|
+
|
|
1206
|
+
def _release_stream_work_lease(self, session: HTTP3Session, stream_id: int) -> None:
|
|
1207
|
+
lease = session.stream_work_leases.pop(stream_id, None)
|
|
1208
|
+
if lease is not None:
|
|
1209
|
+
lease.release()
|
|
1210
|
+
|
|
1211
|
+
def _on_websocket_stream_closed(self, session: HTTP3Session, stream_id: int) -> None:
|
|
1212
|
+
session.websocket_sessions.pop(stream_id, None)
|
|
1213
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1214
|
+
session.h3.abandon_stream(stream_id)
|
|
1215
|
+
|
|
1216
|
+
def _on_webtransport_stream_closed(self, session: HTTP3Session, stream_id: int) -> None:
|
|
1217
|
+
session.webtransport_sessions.pop(stream_id, None)
|
|
1218
|
+
session.webtransport_streams.discard(stream_id)
|
|
1219
|
+
session.webtransport_stream_owners.pop(stream_id, None)
|
|
1220
|
+
session.webtransport_stream_prefaces.pop(stream_id, None)
|
|
1221
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1222
|
+
session.h3.abandon_stream(stream_id)
|
|
1223
|
+
|
|
1224
|
+
def _admit_stream_work(self, session: HTTP3Session, stream_id: int) -> bool:
|
|
1225
|
+
if self.scheduler is None:
|
|
1226
|
+
return True
|
|
1227
|
+
lease = self.scheduler.acquire_work()
|
|
1228
|
+
if lease is None:
|
|
1229
|
+
if self.metrics is not None:
|
|
1230
|
+
self.metrics.scheduler_task_rejected()
|
|
1231
|
+
return False
|
|
1232
|
+
session.stream_work_leases[stream_id] = lease
|
|
1233
|
+
return True
|
|
1234
|
+
|
|
1235
|
+
def _request_target_from_header_map(self, header_map: dict[bytes, bytes]) -> str:
|
|
1236
|
+
method = header_map.get(b':method', b'GET')
|
|
1237
|
+
if method == b'CONNECT' and header_map.get(b':protocol') is None:
|
|
1238
|
+
return header_map.get(b':authority', b'').decode('ascii', 'replace')
|
|
1239
|
+
return header_map.get(b':path', b'/').decode('ascii', 'replace')
|
|
1240
|
+
|
|
1241
|
+
def _build_request(self, request_state: Any, header_map: dict[bytes, bytes]) -> ParsedRequest:
|
|
1242
|
+
method = header_map.get(b':method', b'GET').decode('ascii', 'replace')
|
|
1243
|
+
if method.upper() == 'CONNECT' and header_map.get(b':protocol') is None:
|
|
1244
|
+
target = header_map.get(b':authority', b'').decode('ascii', 'replace')
|
|
1245
|
+
path = target
|
|
1246
|
+
raw_path = target.encode('ascii', 'ignore')
|
|
1247
|
+
query = b''
|
|
1248
|
+
else:
|
|
1249
|
+
target = header_map.get(b':path', b'/').decode('ascii', 'replace')
|
|
1250
|
+
raw_path, _, query = target.encode('ascii', 'ignore').partition(b'?')
|
|
1251
|
+
path = raw_path.decode('utf-8', 'replace')
|
|
1252
|
+
return ParsedRequest(
|
|
1253
|
+
method=method,
|
|
1254
|
+
target=target,
|
|
1255
|
+
path=path,
|
|
1256
|
+
raw_path=raw_path,
|
|
1257
|
+
query_string=query,
|
|
1258
|
+
http_version='3',
|
|
1259
|
+
headers=[(k, v) for k, v in request_state.headers if not k.startswith(b':')],
|
|
1260
|
+
body=request_state.body,
|
|
1261
|
+
keep_alive=True,
|
|
1262
|
+
expect_continue=False,
|
|
1263
|
+
websocket_upgrade=False,
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
async def _start_connect_tunnel_locked(
|
|
1267
|
+
self,
|
|
1268
|
+
session: HTTP3Session,
|
|
1269
|
+
stream_id: int,
|
|
1270
|
+
request_state: Any,
|
|
1271
|
+
header_map: dict[bytes, bytes],
|
|
1272
|
+
endpoint: UDPEndpoint,
|
|
1273
|
+
) -> list[bytes]:
|
|
1274
|
+
authority = header_map.get(b':authority', b'').decode('ascii', 'replace')
|
|
1275
|
+
try:
|
|
1276
|
+
host, port = parse_connect_authority(authority)
|
|
1277
|
+
except Exception:
|
|
1278
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority or '', 400, 'HTTP/3')
|
|
1279
|
+
return self._build_http3_response_datagrams_locked(
|
|
1280
|
+
session,
|
|
1281
|
+
stream_id,
|
|
1282
|
+
400,
|
|
1283
|
+
[(b'content-type', b'text/plain')],
|
|
1284
|
+
b'bad connect target',
|
|
1285
|
+
end_stream=True,
|
|
1286
|
+
)
|
|
1287
|
+
if self.config.http.connect_policy == 'deny':
|
|
1288
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority or '', 403, 'HTTP/3')
|
|
1289
|
+
return self._build_http3_response_datagrams_locked(
|
|
1290
|
+
session,
|
|
1291
|
+
stream_id,
|
|
1292
|
+
403,
|
|
1293
|
+
[(b'content-type', b'text/plain')],
|
|
1294
|
+
b'connect denied',
|
|
1295
|
+
end_stream=True,
|
|
1296
|
+
)
|
|
1297
|
+
if self.config.http.connect_policy == 'allowlist' and not is_connect_allowed(host, port, self.config.http.connect_allow):
|
|
1298
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority or '', 403, 'HTTP/3')
|
|
1299
|
+
return self._build_http3_response_datagrams_locked(
|
|
1300
|
+
session,
|
|
1301
|
+
stream_id,
|
|
1302
|
+
403,
|
|
1303
|
+
[(b'content-type', b'text/plain')],
|
|
1304
|
+
b'connect denied',
|
|
1305
|
+
end_stream=True,
|
|
1306
|
+
)
|
|
1307
|
+
if not self._admit_stream_work(session, stream_id):
|
|
1308
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority or '', 503, 'HTTP/3')
|
|
1309
|
+
return self._build_http3_response_datagrams_locked(
|
|
1310
|
+
session,
|
|
1311
|
+
stream_id,
|
|
1312
|
+
503,
|
|
1313
|
+
[(b'content-type', b'text/plain')],
|
|
1314
|
+
b'scheduler overloaded',
|
|
1315
|
+
end_stream=True,
|
|
1316
|
+
)
|
|
1317
|
+
try:
|
|
1318
|
+
upstream_reader, upstream_writer = await asyncio.wait_for(
|
|
1319
|
+
asyncio.open_connection(host, port),
|
|
1320
|
+
timeout=getattr(self.config, 'read_timeout', 5.0),
|
|
1321
|
+
)
|
|
1322
|
+
except Exception:
|
|
1323
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1324
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority, 502, 'HTTP/3')
|
|
1325
|
+
return self._build_http3_response_datagrams_locked(
|
|
1326
|
+
session,
|
|
1327
|
+
stream_id,
|
|
1328
|
+
502,
|
|
1329
|
+
[(b'content-type', b'text/plain')],
|
|
1330
|
+
b'bad gateway',
|
|
1331
|
+
end_stream=True,
|
|
1332
|
+
)
|
|
1333
|
+
tunnel = _HTTP3ConnectTunnel(
|
|
1334
|
+
handler=self,
|
|
1335
|
+
session=session,
|
|
1336
|
+
stream_id=stream_id,
|
|
1337
|
+
authority=authority,
|
|
1338
|
+
endpoint=endpoint,
|
|
1339
|
+
upstream_reader=upstream_reader,
|
|
1340
|
+
upstream_writer=upstream_writer,
|
|
1341
|
+
work_lease=session.stream_work_leases.get(stream_id),
|
|
1342
|
+
)
|
|
1343
|
+
session.connect_tunnels[stream_id] = tunnel
|
|
1344
|
+
tunnel.start()
|
|
1345
|
+
self.access_logger.log_http(session.addr, 'CONNECT', authority, 200, 'HTTP/3')
|
|
1346
|
+
return self._build_http3_response_datagrams_locked(session, stream_id, 200, [], b'', end_stream=False)
|
|
1347
|
+
|
|
1348
|
+
async def _start_websocket_stream_locked(
|
|
1349
|
+
self,
|
|
1350
|
+
session: HTTP3Session,
|
|
1351
|
+
stream_id: int,
|
|
1352
|
+
request_state: Any,
|
|
1353
|
+
header_map: dict[bytes, bytes],
|
|
1354
|
+
endpoint: UDPEndpoint,
|
|
1355
|
+
) -> list[bytes]:
|
|
1356
|
+
request = self._build_request(request_state, header_map)
|
|
1357
|
+
authority = header_map.get(b':authority')
|
|
1358
|
+
if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
|
|
1359
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 421, 'HTTP/3')
|
|
1360
|
+
return self._build_http3_response_datagrams_locked(
|
|
1361
|
+
session,
|
|
1362
|
+
stream_id,
|
|
1363
|
+
421,
|
|
1364
|
+
[(b'content-type', b'text/plain')],
|
|
1365
|
+
b'misdirected request',
|
|
1366
|
+
end_stream=True,
|
|
1367
|
+
)
|
|
1368
|
+
local = endpoint.local_addr
|
|
1369
|
+
server = (local[0], local[1]) if isinstance(local, tuple) and len(local) >= 2 else ('', None)
|
|
1370
|
+
scheme = header_map.get(
|
|
1371
|
+
b':scheme',
|
|
1372
|
+
self.listener.scheme.encode('ascii', 'ignore') if self.listener.scheme else b'https',
|
|
1373
|
+
).decode('ascii', 'replace')
|
|
1374
|
+
if not self._admit_stream_work(session, stream_id):
|
|
1375
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 503, 'HTTP/3')
|
|
1376
|
+
return self._build_http3_response_datagrams_locked(
|
|
1377
|
+
session,
|
|
1378
|
+
stream_id,
|
|
1379
|
+
503,
|
|
1380
|
+
[(b'content-type', b'text/plain')],
|
|
1381
|
+
b'scheduler overloaded',
|
|
1382
|
+
end_stream=True,
|
|
1383
|
+
)
|
|
1384
|
+
try:
|
|
1385
|
+
websocket = H3WebSocketSession(
|
|
1386
|
+
app=self.app,
|
|
1387
|
+
config=self.config,
|
|
1388
|
+
request=request,
|
|
1389
|
+
client=session.addr,
|
|
1390
|
+
server=server,
|
|
1391
|
+
scheme=scheme,
|
|
1392
|
+
send_headers=lambda status, headers, end_stream: self._send_http3_websocket_headers(
|
|
1393
|
+
session,
|
|
1394
|
+
stream_id,
|
|
1395
|
+
status,
|
|
1396
|
+
headers,
|
|
1397
|
+
end_stream=end_stream,
|
|
1398
|
+
endpoint=endpoint,
|
|
1399
|
+
),
|
|
1400
|
+
send_data=lambda data, end_stream: self._send_http3_websocket_data(
|
|
1401
|
+
session,
|
|
1402
|
+
stream_id,
|
|
1403
|
+
data,
|
|
1404
|
+
end_stream=end_stream,
|
|
1405
|
+
endpoint=endpoint,
|
|
1406
|
+
),
|
|
1407
|
+
metrics=self.metrics,
|
|
1408
|
+
on_close=lambda session=session, stream_id=stream_id: self._on_websocket_stream_closed(session, stream_id),
|
|
1409
|
+
)
|
|
1410
|
+
except ProtocolError:
|
|
1411
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1412
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 400, 'HTTP/3')
|
|
1413
|
+
return self._build_http3_response_datagrams_locked(
|
|
1414
|
+
session,
|
|
1415
|
+
stream_id,
|
|
1416
|
+
400,
|
|
1417
|
+
[(b'content-type', b'text/plain')],
|
|
1418
|
+
b'bad request',
|
|
1419
|
+
end_stream=True,
|
|
1420
|
+
)
|
|
1421
|
+
session.websocket_sessions[stream_id] = websocket
|
|
1422
|
+
await websocket.start()
|
|
1423
|
+
return []
|
|
1424
|
+
|
|
1425
|
+
async def _start_webtransport_stream_locked(
|
|
1426
|
+
self,
|
|
1427
|
+
session: HTTP3Session,
|
|
1428
|
+
stream_id: int,
|
|
1429
|
+
request_state: Any,
|
|
1430
|
+
header_map: dict[bytes, bytes],
|
|
1431
|
+
endpoint: UDPEndpoint,
|
|
1432
|
+
) -> list[bytes]:
|
|
1433
|
+
request = self._build_request(request_state, header_map)
|
|
1434
|
+
authority = header_map.get(b':authority')
|
|
1435
|
+
if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
|
|
1436
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 421, 'HTTP/3')
|
|
1437
|
+
return self._build_http3_response_datagrams_locked(
|
|
1438
|
+
session,
|
|
1439
|
+
stream_id,
|
|
1440
|
+
421,
|
|
1441
|
+
[(b'content-type', b'text/plain')],
|
|
1442
|
+
b'misdirected request',
|
|
1443
|
+
end_stream=True,
|
|
1444
|
+
)
|
|
1445
|
+
if not self._admit_stream_work(session, stream_id):
|
|
1446
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 503, 'HTTP/3')
|
|
1447
|
+
return self._build_http3_response_datagrams_locked(
|
|
1448
|
+
session,
|
|
1449
|
+
stream_id,
|
|
1450
|
+
503,
|
|
1451
|
+
[(b'content-type', b'text/plain')],
|
|
1452
|
+
b'scheduler overloaded',
|
|
1453
|
+
end_stream=True,
|
|
1454
|
+
)
|
|
1455
|
+
response_headers: list[tuple[bytes, bytes]] = []
|
|
1456
|
+
draft = next((value for name, value in request_state.headers if name.lower() == b'sec-webtransport-http3-draft'), None)
|
|
1457
|
+
if draft:
|
|
1458
|
+
response_headers.append((b'sec-webtransport-http3-draft', draft))
|
|
1459
|
+
session.webtransport_streams.add(stream_id)
|
|
1460
|
+
session.webtransport_stream_owners[stream_id] = stream_id
|
|
1461
|
+
local = endpoint.local_addr
|
|
1462
|
+
server = (local[0], local[1]) if isinstance(local, tuple) and len(local) >= 2 else ('', None)
|
|
1463
|
+
webtransport = _HTTP3WebTransportSession(
|
|
1464
|
+
handler=self,
|
|
1465
|
+
session=session,
|
|
1466
|
+
stream_id=stream_id,
|
|
1467
|
+
request=request,
|
|
1468
|
+
client=session.addr,
|
|
1469
|
+
server=server,
|
|
1470
|
+
scheme='https' if self.listener.scheme in {'https', 'wss'} else self.listener.scheme,
|
|
1471
|
+
endpoint=endpoint,
|
|
1472
|
+
work_lease=session.stream_work_leases.get(stream_id),
|
|
1473
|
+
)
|
|
1474
|
+
session.webtransport_sessions[stream_id] = webtransport
|
|
1475
|
+
await webtransport.start()
|
|
1476
|
+
self.access_logger.log_http(session.addr, 'CONNECT', request.path, 200, 'HTTP/3')
|
|
1477
|
+
return self._build_http3_response_datagrams_locked(session, stream_id, 200, response_headers, b'', end_stream=False)
|
|
1478
|
+
|
|
1479
|
+
async def _drain_connect_request_body_locked(
|
|
1480
|
+
self,
|
|
1481
|
+
session: HTTP3Session,
|
|
1482
|
+
stream_id: int,
|
|
1483
|
+
request_state: Any,
|
|
1484
|
+
) -> None:
|
|
1485
|
+
tunnel = session.connect_tunnels.get(stream_id)
|
|
1486
|
+
if tunnel is None:
|
|
1487
|
+
return
|
|
1488
|
+
chunks = list(request_state.body_parts)
|
|
1489
|
+
request_state.body_parts.clear()
|
|
1490
|
+
await tunnel.feed_client_data(chunks, end_stream=request_state.ended, already_locked=True)
|
|
1491
|
+
|
|
1492
|
+
async def _drain_websocket_request_body_locked(
|
|
1493
|
+
self,
|
|
1494
|
+
session: HTTP3Session,
|
|
1495
|
+
stream_id: int,
|
|
1496
|
+
request_state: Any,
|
|
1497
|
+
endpoint: UDPEndpoint,
|
|
1498
|
+
) -> None:
|
|
1499
|
+
websocket = session.websocket_sessions.get(stream_id)
|
|
1500
|
+
if websocket is None:
|
|
1501
|
+
return
|
|
1502
|
+
chunks = list(request_state.body_parts)
|
|
1503
|
+
request_state.body_parts.clear()
|
|
1504
|
+
try:
|
|
1505
|
+
await websocket.feed_data(b''.join(chunks), end_stream=request_state.ended)
|
|
1506
|
+
except Exception:
|
|
1507
|
+
await self._reset_http3_websocket_stream(
|
|
1508
|
+
session,
|
|
1509
|
+
stream_id,
|
|
1510
|
+
endpoint,
|
|
1511
|
+
already_locked=True,
|
|
1512
|
+
)
|
|
1513
|
+
await websocket.abort()
|
|
1514
|
+
|
|
1515
|
+
async def _drain_webtransport_request_body_locked(
|
|
1516
|
+
self,
|
|
1517
|
+
session: HTTP3Session,
|
|
1518
|
+
stream_id: int,
|
|
1519
|
+
request_state: Any,
|
|
1520
|
+
) -> None:
|
|
1521
|
+
webtransport = session.webtransport_sessions.get(stream_id)
|
|
1522
|
+
if webtransport is None:
|
|
1523
|
+
return
|
|
1524
|
+
chunks = list(request_state.body_parts)
|
|
1525
|
+
request_state.body_parts.clear()
|
|
1526
|
+
await webtransport.feed_connect_stream_data(b''.join(chunks), end_stream=request_state.ended)
|
|
1527
|
+
|
|
1528
|
+
async def _respond_ready_requests(self, session: HTTP3Session, endpoint: UDPEndpoint) -> list[bytes]:
|
|
1529
|
+
outbound: list[bytes] = []
|
|
1530
|
+
for request_state in session.h3.ready_request_states():
|
|
1531
|
+
stream_id = request_state.stream_id
|
|
1532
|
+
if not request_state.ended or stream_id in session.responded_streams:
|
|
1533
|
+
continue
|
|
1534
|
+
outbound.extend(await self._invoke_http_app(session, stream_id, request_state, endpoint))
|
|
1535
|
+
session.responded_streams.add(stream_id)
|
|
1536
|
+
return outbound
|
|
1537
|
+
|
|
1538
|
+
def _validate_request_headers(self, headers: list[tuple[bytes, bytes]]) -> dict[bytes, bytes]:
|
|
1539
|
+
pseudo_seen: set[bytes] = set()
|
|
1540
|
+
regular_seen = False
|
|
1541
|
+
header_map: dict[bytes, bytes] = {}
|
|
1542
|
+
for name, value in headers:
|
|
1543
|
+
if any(65 <= byte <= 90 for byte in name):
|
|
1544
|
+
raise ProtocolError('uppercase header field name forbidden')
|
|
1545
|
+
if name.startswith(b':'):
|
|
1546
|
+
if regular_seen:
|
|
1547
|
+
raise ProtocolError('pseudo-header after regular header')
|
|
1548
|
+
if name not in {b':method', b':scheme', b':authority', b':path', b':protocol'}:
|
|
1549
|
+
raise ProtocolError('invalid request pseudo-header')
|
|
1550
|
+
if name in pseudo_seen:
|
|
1551
|
+
raise ProtocolError('duplicate pseudo-header')
|
|
1552
|
+
pseudo_seen.add(name)
|
|
1553
|
+
else:
|
|
1554
|
+
regular_seen = True
|
|
1555
|
+
if name in {b'connection', b'upgrade', b'proxy-connection', b'transfer-encoding'}:
|
|
1556
|
+
raise ProtocolError('connection-specific header forbidden')
|
|
1557
|
+
if name == b'te' and value.lower() != b'trailers':
|
|
1558
|
+
raise ProtocolError('invalid TE header')
|
|
1559
|
+
header_map[name] = value
|
|
1560
|
+
if b':method' not in pseudo_seen:
|
|
1561
|
+
raise ProtocolError('missing :method pseudo-header')
|
|
1562
|
+
method = header_map.get(b':method', b'GET')
|
|
1563
|
+
protocol = header_map.get(b':protocol')
|
|
1564
|
+
if protocol is not None:
|
|
1565
|
+
if method != b'CONNECT':
|
|
1566
|
+
raise ProtocolError('extended CONNECT requires CONNECT method')
|
|
1567
|
+
if b':scheme' not in pseudo_seen or b':path' not in pseudo_seen or b':authority' not in pseudo_seen:
|
|
1568
|
+
raise ProtocolError('extended CONNECT missing required pseudo-headers')
|
|
1569
|
+
return header_map
|
|
1570
|
+
if method == b'CONNECT':
|
|
1571
|
+
if b':authority' not in pseudo_seen:
|
|
1572
|
+
raise ProtocolError('CONNECT missing :authority pseudo-header')
|
|
1573
|
+
if b':scheme' in pseudo_seen or b':path' in pseudo_seen:
|
|
1574
|
+
raise ProtocolError('CONNECT must not include :scheme or :path pseudo-headers')
|
|
1575
|
+
return header_map
|
|
1576
|
+
if b':scheme' not in pseudo_seen or b':path' not in pseudo_seen:
|
|
1577
|
+
raise ProtocolError('missing required request pseudo-header')
|
|
1578
|
+
return header_map
|
|
1579
|
+
|
|
1580
|
+
async def _invoke_http_app(self, session: HTTP3Session, stream_id: int, request_state: Any, endpoint: UDPEndpoint) -> list[bytes]:
|
|
1581
|
+
try:
|
|
1582
|
+
header_map = self._validate_request_headers(list(request_state.headers))
|
|
1583
|
+
scheme = header_map.get(b':scheme', self.listener.scheme.encode('ascii', 'ignore') if self.listener.scheme else b'https').decode('ascii', 'replace')
|
|
1584
|
+
except ProtocolError:
|
|
1585
|
+
header_lines = [(b':status', b'400'), (b'content-type', b'text/plain')]
|
|
1586
|
+
header_block = session.h3.encode_headers(stream_id, header_lines)
|
|
1587
|
+
payload = encode_frame(FRAME_HEADERS, header_block) + encode_frame(FRAME_DATA, b'bad request')
|
|
1588
|
+
return [*self._flush_qpack_streams(session), session.quic.send_stream_data(stream_id, payload, fin=True)]
|
|
1589
|
+
if not self._admit_stream_work(session, stream_id):
|
|
1590
|
+
return self._build_http3_response_datagrams_locked(
|
|
1591
|
+
session,
|
|
1592
|
+
stream_id,
|
|
1593
|
+
503,
|
|
1594
|
+
[(b'content-type', b'text/plain')],
|
|
1595
|
+
b'scheduler overloaded',
|
|
1596
|
+
end_stream=True,
|
|
1597
|
+
)
|
|
1598
|
+
request = self._build_request(request_state, header_map)
|
|
1599
|
+
client = session.addr
|
|
1600
|
+
local = endpoint.local_addr
|
|
1601
|
+
server = (local[0], local[1]) if isinstance(local, tuple) and len(local) >= 2 else ('', None)
|
|
1602
|
+
extensions = {}
|
|
1603
|
+
raw_request_trailers = list(getattr(request_state, 'trailers', ()))
|
|
1604
|
+
try:
|
|
1605
|
+
request_trailers = apply_request_trailer_policy(raw_request_trailers, self.config.http.trailer_policy)
|
|
1606
|
+
except ProtocolError:
|
|
1607
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1608
|
+
return self._build_http3_response_datagrams_locked(
|
|
1609
|
+
session,
|
|
1610
|
+
stream_id,
|
|
1611
|
+
400,
|
|
1612
|
+
[(b'content-type', b'text/plain')],
|
|
1613
|
+
b'bad request trailers',
|
|
1614
|
+
end_stream=True,
|
|
1615
|
+
)
|
|
1616
|
+
if request.method.upper() == 'CONNECT':
|
|
1617
|
+
extensions['tigrcorn.http.connect'] = {'authority': request.target}
|
|
1618
|
+
if request_trailers and self.config.http.trailer_policy != 'drop':
|
|
1619
|
+
extensions['tigrcorn.http.request_trailers'] = {}
|
|
1620
|
+
extensions['tigrcorn.http.response.file'] = {'protocol': 'http/3', 'streaming': True, 'sendfile': False}
|
|
1621
|
+
extensions['http.response.pathsend'] = {}
|
|
1622
|
+
authority = header_map.get(b':authority')
|
|
1623
|
+
if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
|
|
1624
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1625
|
+
self.access_logger.log_http(client, request.method, request.path, 421, 'HTTP/3')
|
|
1626
|
+
return self._build_http3_response_datagrams_locked(
|
|
1627
|
+
session,
|
|
1628
|
+
stream_id,
|
|
1629
|
+
421,
|
|
1630
|
+
[(b'content-type', b'text/plain')],
|
|
1631
|
+
b'misdirected request',
|
|
1632
|
+
end_stream=True,
|
|
1633
|
+
)
|
|
1634
|
+
if self._should_send_too_early(session):
|
|
1635
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1636
|
+
self.access_logger.log_http(client, request.method, request.path, 425, 'HTTP/3')
|
|
1637
|
+
return self._build_http3_response_datagrams_locked(
|
|
1638
|
+
session,
|
|
1639
|
+
stream_id,
|
|
1640
|
+
425,
|
|
1641
|
+
[(b'content-type', b'text/plain')],
|
|
1642
|
+
b'too early',
|
|
1643
|
+
end_stream=True,
|
|
1644
|
+
)
|
|
1645
|
+
scope = build_http_scope(request, client=client, server=server, scheme=scheme, extensions=extensions, root_path=self.config.proxy.root_path, proxy=self.config.proxy)
|
|
1646
|
+
receive = HTTPRequestReceive(request.body, trailers=request_trailers, trailer_policy=self.config.http.trailer_policy)
|
|
1647
|
+
send = HTTPResponseCollector()
|
|
1648
|
+
status = 500
|
|
1649
|
+
try:
|
|
1650
|
+
try:
|
|
1651
|
+
await self.app(scope, receive, send)
|
|
1652
|
+
send.finalize()
|
|
1653
|
+
assert send.status is not None
|
|
1654
|
+
status = send.status
|
|
1655
|
+
headers = list(send.headers)
|
|
1656
|
+
trailers = list(send.trailers)
|
|
1657
|
+
informational = list(send.informational_responses)
|
|
1658
|
+
body_segments = list(send.body_segments) if send.uses_streamed_body else None
|
|
1659
|
+
if body_segments is None and send.has_spooled_body():
|
|
1660
|
+
spooled_segments = send.spooled_body_segments()
|
|
1661
|
+
spooled_path = ''
|
|
1662
|
+
if spooled_segments:
|
|
1663
|
+
first_segment = spooled_segments[0]
|
|
1664
|
+
spooled_path = getattr(first_segment, 'path', '')
|
|
1665
|
+
planned = plan_file_backed_response_entity_semantics(
|
|
1666
|
+
method=request.method,
|
|
1667
|
+
request_headers=request.headers,
|
|
1668
|
+
response_headers=headers,
|
|
1669
|
+
status=status,
|
|
1670
|
+
body_path=spooled_path,
|
|
1671
|
+
body_length=send.body_length,
|
|
1672
|
+
generated_etag=send.generated_entity_tag(),
|
|
1673
|
+
apply_content_coding=True,
|
|
1674
|
+
trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
|
|
1675
|
+
)
|
|
1676
|
+
if planned.requires_materialization:
|
|
1677
|
+
body = await send.materialize_body()
|
|
1678
|
+
processed = apply_response_entity_semantics(
|
|
1679
|
+
method=request.method,
|
|
1680
|
+
request_headers=request.headers,
|
|
1681
|
+
response_headers=headers,
|
|
1682
|
+
body=body,
|
|
1683
|
+
status=status,
|
|
1684
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
1685
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
1686
|
+
apply_content_coding=True,
|
|
1687
|
+
generate_etag=True,
|
|
1688
|
+
)
|
|
1689
|
+
status = processed.status
|
|
1690
|
+
headers = processed.headers
|
|
1691
|
+
body = processed.body
|
|
1692
|
+
if processed.head_response:
|
|
1693
|
+
trailers = []
|
|
1694
|
+
elif planned.use_body_segments:
|
|
1695
|
+
status = planned.status
|
|
1696
|
+
headers = planned.headers
|
|
1697
|
+
body_segments = list(planned.body_segments)
|
|
1698
|
+
body = b''
|
|
1699
|
+
else:
|
|
1700
|
+
status = planned.status
|
|
1701
|
+
headers = planned.headers
|
|
1702
|
+
body = planned.body
|
|
1703
|
+
trailers = []
|
|
1704
|
+
elif body_segments is None:
|
|
1705
|
+
body = await send.materialize_body()
|
|
1706
|
+
processed = apply_response_entity_semantics(
|
|
1707
|
+
method=request.method,
|
|
1708
|
+
request_headers=request.headers,
|
|
1709
|
+
response_headers=headers,
|
|
1710
|
+
body=body,
|
|
1711
|
+
status=status,
|
|
1712
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
1713
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
1714
|
+
apply_content_coding=True,
|
|
1715
|
+
generate_etag=True,
|
|
1716
|
+
)
|
|
1717
|
+
status = processed.status
|
|
1718
|
+
headers = processed.headers
|
|
1719
|
+
body = processed.body
|
|
1720
|
+
if processed.head_response:
|
|
1721
|
+
trailers = []
|
|
1722
|
+
except Exception:
|
|
1723
|
+
send.cleanup()
|
|
1724
|
+
status, headers, body, trailers = 500, [(b'content-type', b'text/plain')], b'internal server error', []
|
|
1725
|
+
informational = []
|
|
1726
|
+
body_segments = None
|
|
1727
|
+
if body_segments is not None:
|
|
1728
|
+
await self._send_http3_streamed_response_locked(
|
|
1729
|
+
session,
|
|
1730
|
+
stream_id,
|
|
1731
|
+
status,
|
|
1732
|
+
headers,
|
|
1733
|
+
body_segments,
|
|
1734
|
+
trailers,
|
|
1735
|
+
informational,
|
|
1736
|
+
endpoint,
|
|
1737
|
+
)
|
|
1738
|
+
if self.metrics is not None:
|
|
1739
|
+
self.metrics.http3_request_served()
|
|
1740
|
+
self.access_logger.log_http(client, request.method, request.path, status, 'HTTP/3')
|
|
1741
|
+
self.sessions[session.addr] = session
|
|
1742
|
+
return []
|
|
1743
|
+
headers = apply_response_header_policy(
|
|
1744
|
+
strip_connection_specific_headers(headers),
|
|
1745
|
+
server_header=self.config.server_header_value,
|
|
1746
|
+
include_date_header=self.config.include_date_header,
|
|
1747
|
+
default_headers=self.config.default_response_headers,
|
|
1748
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version='3'),
|
|
1749
|
+
)
|
|
1750
|
+
frame_payload = bytearray()
|
|
1751
|
+
for interim_status, interim_headers in informational:
|
|
1752
|
+
interim_header_block = session.h3.encode_headers(
|
|
1753
|
+
stream_id,
|
|
1754
|
+
[(b':status', str(interim_status).encode('ascii')), *sanitize_early_hints_headers(interim_headers)],
|
|
1755
|
+
)
|
|
1756
|
+
frame_payload.extend(encode_frame(FRAME_HEADERS, interim_header_block))
|
|
1757
|
+
header_lines = [(b':status', str(status).encode('ascii')), *headers]
|
|
1758
|
+
header_block = session.h3.encode_headers(stream_id, header_lines)
|
|
1759
|
+
qpack_outbound = self._flush_qpack_streams(session)
|
|
1760
|
+
frame_payload.extend(encode_frame(FRAME_HEADERS, header_block))
|
|
1761
|
+
if body:
|
|
1762
|
+
frame_payload.extend(encode_frame(FRAME_DATA, body))
|
|
1763
|
+
if trailers:
|
|
1764
|
+
trailer_block = session.h3.encode_headers(stream_id, list(trailers))
|
|
1765
|
+
frame_payload.extend(encode_frame(FRAME_HEADERS, trailer_block))
|
|
1766
|
+
self.access_logger.log_http(client, request.method, request.path, status, 'HTTP/3')
|
|
1767
|
+
self.sessions[session.addr] = session
|
|
1768
|
+
if self.metrics is not None:
|
|
1769
|
+
self.metrics.http3_request_served()
|
|
1770
|
+
return [*qpack_outbound, session.quic.send_stream_data(stream_id, bytes(frame_payload), fin=True)]
|
|
1771
|
+
finally:
|
|
1772
|
+
send.cleanup()
|
|
1773
|
+
self._release_stream_work_lease(session, stream_id)
|
|
1774
|
+
|
|
1775
|
+
async def _invoke_custom_quic_app(self, session: HTTP3Session, event: Any, endpoint: UDPEndpoint) -> list[bytes]:
|
|
1776
|
+
client = session.addr
|
|
1777
|
+
local = endpoint.local_addr
|
|
1778
|
+
server = (local[0], local[1]) if isinstance(local, tuple) and len(local) >= 2 else ('', None)
|
|
1779
|
+
scope = adapt_scope(
|
|
1780
|
+
build_custom_scope(
|
|
1781
|
+
'tigrcorn.quic',
|
|
1782
|
+
scheme=self.listener.scheme or 'quic',
|
|
1783
|
+
client=client,
|
|
1784
|
+
server=server,
|
|
1785
|
+
stream_id=event.stream_id,
|
|
1786
|
+
packet_number=event.packet_number,
|
|
1787
|
+
extensions={'tigrcorn.custom': {'transport': 'udp', 'protocol': 'quic'}},
|
|
1788
|
+
)
|
|
1789
|
+
)
|
|
1790
|
+
receive = _SingleEventReceive({'type': 'tigrcorn.stream.receive', 'data': event.data, 'more_data': not bool(event.fin)})
|
|
1791
|
+
send = _CustomQuicSend(session=session, stream_id=event.stream_id)
|
|
1792
|
+
await self.app(scope, receive, send)
|
|
1793
|
+
return send.flush()
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
class _SingleEventReceive:
|
|
1797
|
+
def __init__(self, event: dict) -> None:
|
|
1798
|
+
self.event = event
|
|
1799
|
+
self.sent = False
|
|
1800
|
+
|
|
1801
|
+
async def __call__(self) -> dict:
|
|
1802
|
+
if not self.sent:
|
|
1803
|
+
self.sent = True
|
|
1804
|
+
return self.event
|
|
1805
|
+
return {'type': 'tigrcorn.stream.disconnect'}
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
class _CustomQuicSend:
|
|
1809
|
+
def __init__(self, *, session: HTTP3Session, stream_id: int | None) -> None:
|
|
1810
|
+
self.session = session
|
|
1811
|
+
self.stream_id = 0 if stream_id is None else stream_id
|
|
1812
|
+
self.messages: list[bytes] = []
|
|
1813
|
+
|
|
1814
|
+
async def __call__(self, message: dict) -> None:
|
|
1815
|
+
typ = message.get('type')
|
|
1816
|
+
if typ != 'tigrcorn.stream.send':
|
|
1817
|
+
raise RuntimeError(f'unexpected custom quic send event: {typ!r}')
|
|
1818
|
+
data = bytes(message.get('data', b''))
|
|
1819
|
+
fin = not bool(message.get('more_data', False))
|
|
1820
|
+
self.messages.append(self.session.quic.send_stream_data(self.stream_id, data, fin=fin))
|
|
1821
|
+
|
|
1822
|
+
def flush(self) -> list[bytes]:
|
|
1823
|
+
return list(self.messages)
|