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,1303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from tigrcorn_asgi.receive import HTTPRequestReceive, apply_request_trailer_policy
|
|
8
|
+
from tigrcorn_asgi.scopes.http import build_http_scope
|
|
9
|
+
from tigrcorn_asgi.send import HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
|
|
10
|
+
from tigrcorn_config.model import ServerConfig
|
|
11
|
+
from tigrcorn_protocols.flow.keepalive import KeepAlivePolicy, KeepAliveRuntime
|
|
12
|
+
from tigrcorn_core.constants import H2_PREFACE
|
|
13
|
+
from tigrcorn_core.errors import ProtocolError
|
|
14
|
+
from tigrcorn_observability.metrics import Metrics
|
|
15
|
+
from tigrcorn_observability.logging import AccessLogger
|
|
16
|
+
from tigrcorn_http.alt_svc import configured_alt_svc_values
|
|
17
|
+
from tigrcorn_http.entity import apply_response_entity_semantics, plan_file_backed_response_entity_semantics
|
|
18
|
+
from tigrcorn_protocols.http1.parser import ParsedRequest
|
|
19
|
+
from tigrcorn_protocols.http2.codec import (
|
|
20
|
+
DEFAULT_SETTINGS,
|
|
21
|
+
H2_CONNECT_ERROR,
|
|
22
|
+
FLAG_ACK,
|
|
23
|
+
FLAG_END_HEADERS,
|
|
24
|
+
FLAG_END_STREAM,
|
|
25
|
+
FRAME_CONTINUATION,
|
|
26
|
+
FRAME_DATA,
|
|
27
|
+
FRAME_GOAWAY,
|
|
28
|
+
FRAME_HEADERS,
|
|
29
|
+
FRAME_PING,
|
|
30
|
+
FRAME_PRIORITY,
|
|
31
|
+
FRAME_PUSH_PROMISE,
|
|
32
|
+
FRAME_RST_STREAM,
|
|
33
|
+
FRAME_SETTINGS,
|
|
34
|
+
FRAME_WINDOW_UPDATE,
|
|
35
|
+
FrameBuffer,
|
|
36
|
+
FrameWriter,
|
|
37
|
+
HTTP2Frame,
|
|
38
|
+
decode_settings,
|
|
39
|
+
headers_payload_fragment,
|
|
40
|
+
parse_goaway,
|
|
41
|
+
parse_priority,
|
|
42
|
+
parse_push_promise,
|
|
43
|
+
parse_window_update,
|
|
44
|
+
serialize_goaway,
|
|
45
|
+
serialize_ping,
|
|
46
|
+
serialize_push_promise,
|
|
47
|
+
serialize_rst_stream,
|
|
48
|
+
serialize_settings,
|
|
49
|
+
serialize_settings_ack,
|
|
50
|
+
SETTING_ENABLE_CONNECT_PROTOCOL,
|
|
51
|
+
SETTING_ENABLE_PUSH,
|
|
52
|
+
SETTING_INITIAL_WINDOW_SIZE,
|
|
53
|
+
SETTING_MAX_CONCURRENT_STREAMS,
|
|
54
|
+
SETTING_MAX_FRAME_SIZE,
|
|
55
|
+
SETTING_MAX_HEADER_LIST_SIZE,
|
|
56
|
+
serialize_window_update,
|
|
57
|
+
strip_padding,
|
|
58
|
+
)
|
|
59
|
+
from tigrcorn_protocols.http2.flow import FlowWaiter, next_adaptive_window_target
|
|
60
|
+
from tigrcorn_protocols.http2.hpack import HPACKDecoder, HPACKEncoder
|
|
61
|
+
from tigrcorn_protocols.http2.state import H2ConnectionState, H2StreamLifecycle, H2StreamState
|
|
62
|
+
from tigrcorn_protocols.scheduler.runtime import ProductionScheduler, WorkLease
|
|
63
|
+
from tigrcorn_protocols.http2.streams import H2StreamRegistry
|
|
64
|
+
from tigrcorn_protocols.connect import close_tcp_writer, half_close_tcp_writer, is_connect_allowed, parse_connect_authority
|
|
65
|
+
from tigrcorn_protocols.http2.websocket import H2WebSocketSession
|
|
66
|
+
from tigrcorn_core.types import ASGIApp
|
|
67
|
+
from tigrcorn_core.utils.authority import authority_allowed
|
|
68
|
+
from tigrcorn_core.utils.headers import apply_response_header_policy, sanitize_early_hints_headers, strip_connection_specific_headers
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _HTTP2ConnectTunnel:
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
handler: HTTP2ConnectionHandler,
|
|
76
|
+
stream_id: int,
|
|
77
|
+
authority: str,
|
|
78
|
+
upstream_reader: asyncio.StreamReader,
|
|
79
|
+
upstream_writer: asyncio.StreamWriter,
|
|
80
|
+
work_lease: WorkLease | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self.handler = handler
|
|
83
|
+
self.stream_id = stream_id
|
|
84
|
+
self.authority = authority
|
|
85
|
+
self.upstream_reader = upstream_reader
|
|
86
|
+
self.upstream_writer = upstream_writer
|
|
87
|
+
self.work_lease = work_lease
|
|
88
|
+
self.relay_task: asyncio.Task[None] | None = None
|
|
89
|
+
self.client_input_closed = False
|
|
90
|
+
self.server_output_closed = False
|
|
91
|
+
self.closed = False
|
|
92
|
+
|
|
93
|
+
async def start(self) -> None:
|
|
94
|
+
try:
|
|
95
|
+
await self.handler._send_stream_headers(self.stream_id, 200, [], end_stream=False)
|
|
96
|
+
except Exception:
|
|
97
|
+
await close_tcp_writer(self.upstream_writer)
|
|
98
|
+
raise
|
|
99
|
+
self.relay_task = asyncio.create_task(
|
|
100
|
+
self._relay_upstream_to_client(),
|
|
101
|
+
name=f'tigrcorn-h2-connect-{self.stream_id}',
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def feed_client_data(self, data: bytes, *, end_stream: bool) -> None:
|
|
105
|
+
if self.closed:
|
|
106
|
+
return
|
|
107
|
+
try:
|
|
108
|
+
if data:
|
|
109
|
+
self.upstream_writer.write(data)
|
|
110
|
+
await self.upstream_writer.drain()
|
|
111
|
+
if end_stream and not self.client_input_closed:
|
|
112
|
+
self.client_input_closed = True
|
|
113
|
+
await half_close_tcp_writer(self.upstream_writer)
|
|
114
|
+
except Exception:
|
|
115
|
+
await self.handler._reset_connect_stream(self.stream_id)
|
|
116
|
+
await self.abort()
|
|
117
|
+
return
|
|
118
|
+
await self._finish_if_complete()
|
|
119
|
+
|
|
120
|
+
async def abort(self) -> None:
|
|
121
|
+
if self.closed:
|
|
122
|
+
return
|
|
123
|
+
self.closed = True
|
|
124
|
+
current = asyncio.current_task()
|
|
125
|
+
if self.relay_task is not None and self.relay_task is not current:
|
|
126
|
+
self.relay_task.cancel()
|
|
127
|
+
with suppress(asyncio.CancelledError):
|
|
128
|
+
await self.relay_task
|
|
129
|
+
state = self.handler.streams.find(self.stream_id)
|
|
130
|
+
if state is not None and state.connect_tunnel is self:
|
|
131
|
+
state.connect_tunnel = None
|
|
132
|
+
if self.work_lease is not None:
|
|
133
|
+
self.work_lease.release()
|
|
134
|
+
await close_tcp_writer(self.upstream_writer)
|
|
135
|
+
self.handler._finalize_stream_if_complete(self.stream_id)
|
|
136
|
+
|
|
137
|
+
async def _relay_upstream_to_client(self) -> None:
|
|
138
|
+
reset_stream = False
|
|
139
|
+
try:
|
|
140
|
+
while True:
|
|
141
|
+
chunk = await asyncio.wait_for(self.upstream_reader.read(65536), timeout=self.handler.config.http.idle_timeout)
|
|
142
|
+
if not chunk:
|
|
143
|
+
break
|
|
144
|
+
await self.handler._send_stream_data(self.stream_id, chunk, end_stream=False)
|
|
145
|
+
except asyncio.CancelledError:
|
|
146
|
+
raise
|
|
147
|
+
except Exception:
|
|
148
|
+
reset_stream = True
|
|
149
|
+
else:
|
|
150
|
+
try:
|
|
151
|
+
await self.handler._send_stream_data(self.stream_id, b'', end_stream=True)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
finally:
|
|
155
|
+
self.server_output_closed = True
|
|
156
|
+
if reset_stream:
|
|
157
|
+
with suppress(Exception):
|
|
158
|
+
await self.handler._reset_connect_stream(self.stream_id)
|
|
159
|
+
await self._finish_if_complete()
|
|
160
|
+
|
|
161
|
+
async def _finish_if_complete(self) -> None:
|
|
162
|
+
if self.client_input_closed and self.server_output_closed:
|
|
163
|
+
await self.abort()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class HTTP2ConnectionHandler:
|
|
167
|
+
def __init__(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
app: ASGIApp,
|
|
171
|
+
config: ServerConfig,
|
|
172
|
+
access_logger: AccessLogger,
|
|
173
|
+
scheduler: ProductionScheduler | None = None,
|
|
174
|
+
metrics: Metrics | None = None,
|
|
175
|
+
reader: asyncio.StreamReader,
|
|
176
|
+
writer: asyncio.StreamWriter,
|
|
177
|
+
client: tuple[str, int] | None,
|
|
178
|
+
server: tuple[str, int] | tuple[str, None] | None,
|
|
179
|
+
scheme: str,
|
|
180
|
+
prebuffer: bytes = b"",
|
|
181
|
+
scope_extensions: dict | None = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
self.app = app
|
|
184
|
+
self.config = config
|
|
185
|
+
self.access_logger = access_logger
|
|
186
|
+
self.scheduler = scheduler
|
|
187
|
+
self.metrics = metrics
|
|
188
|
+
self.reader = reader
|
|
189
|
+
self.writer = writer
|
|
190
|
+
self.client = client
|
|
191
|
+
self.server = server
|
|
192
|
+
self.scheme = scheme
|
|
193
|
+
self.prebuffer = prebuffer
|
|
194
|
+
self.scope_extensions = dict(scope_extensions or {})
|
|
195
|
+
self.state = H2ConnectionState()
|
|
196
|
+
self.state.local_settings[SETTING_MAX_CONCURRENT_STREAMS] = self.config.http.http2_max_concurrent_streams
|
|
197
|
+
self.state.local_settings[SETTING_MAX_HEADER_LIST_SIZE] = self.config.http.http2_max_headers_size
|
|
198
|
+
self.state.local_settings[SETTING_MAX_FRAME_SIZE] = self.config.http.http2_max_frame_size
|
|
199
|
+
self.state.local_settings[SETTING_INITIAL_WINDOW_SIZE] = self.config.http.http2_initial_stream_window_size
|
|
200
|
+
self.state.connection_receive_window_target = self.config.http.http2_initial_connection_window_size
|
|
201
|
+
self._initial_connection_window_increment = max(
|
|
202
|
+
0,
|
|
203
|
+
self.state.connection_receive_window_target - DEFAULT_SETTINGS[SETTING_INITIAL_WINDOW_SIZE],
|
|
204
|
+
)
|
|
205
|
+
if self._initial_connection_window_increment:
|
|
206
|
+
self.state.connection_receive_window.increase(self._initial_connection_window_increment)
|
|
207
|
+
self.streams = H2StreamRegistry()
|
|
208
|
+
self.stream_tasks: dict[int, asyncio.Task[None]] = {}
|
|
209
|
+
self.stream_work_leases: dict[int, WorkLease] = {}
|
|
210
|
+
self.frame_buffer = FrameBuffer()
|
|
211
|
+
self.frame_writer = FrameWriter(self.state.max_frame_size)
|
|
212
|
+
self.writer_lock = asyncio.Lock()
|
|
213
|
+
self.waiters: dict[int, FlowWaiter] = {}
|
|
214
|
+
self.hpack_decoder = HPACKDecoder(
|
|
215
|
+
max_table_size=DEFAULT_SETTINGS[0x1],
|
|
216
|
+
max_header_list_size=self.state.max_header_list_size,
|
|
217
|
+
max_header_block_size=self.config.http.http2_max_headers_size,
|
|
218
|
+
)
|
|
219
|
+
self.hpack_encoder = HPACKEncoder(max_table_size=DEFAULT_SETTINGS[0x1])
|
|
220
|
+
self.keepalive_policy = KeepAlivePolicy(
|
|
221
|
+
idle_timeout=self.config.http.idle_timeout,
|
|
222
|
+
ping_interval=self.config.http.http2_keep_alive_interval,
|
|
223
|
+
ping_timeout=self.config.http.http2_keep_alive_timeout,
|
|
224
|
+
)
|
|
225
|
+
self.keepalive = KeepAliveRuntime(self.keepalive_policy) if self.keepalive_policy.enabled else None
|
|
226
|
+
self.keepalive_task: asyncio.Task[None] | None = None
|
|
227
|
+
self.running = True
|
|
228
|
+
self._continuation_stream_id: int | None = None
|
|
229
|
+
|
|
230
|
+
def _record_keepalive_activity(self) -> None:
|
|
231
|
+
if self.keepalive is not None:
|
|
232
|
+
self.keepalive.record_activity()
|
|
233
|
+
|
|
234
|
+
async def _keepalive_loop(self) -> None:
|
|
235
|
+
while self.running and not self.writer.is_closing():
|
|
236
|
+
await asyncio.sleep(0.05)
|
|
237
|
+
if self.keepalive is None or not self.running:
|
|
238
|
+
return
|
|
239
|
+
if not self.state.remote_settings_seen:
|
|
240
|
+
continue
|
|
241
|
+
if self.keepalive.ping_timed_out():
|
|
242
|
+
self.running = False
|
|
243
|
+
self.writer.close()
|
|
244
|
+
with suppress(Exception):
|
|
245
|
+
await self.writer.wait_closed()
|
|
246
|
+
return
|
|
247
|
+
payload = self.keepalive.next_ping_payload()
|
|
248
|
+
if payload is None:
|
|
249
|
+
continue
|
|
250
|
+
await self._write_raw(serialize_ping(payload, ack=False), record_activity=False)
|
|
251
|
+
|
|
252
|
+
async def handle(self) -> None:
|
|
253
|
+
await self._ensure_preface()
|
|
254
|
+
try:
|
|
255
|
+
await self._write_raw(serialize_settings(self.state.local_settings))
|
|
256
|
+
if self._initial_connection_window_increment:
|
|
257
|
+
await self._write_raw(serialize_window_update(0, self._initial_connection_window_increment))
|
|
258
|
+
if self.keepalive is not None:
|
|
259
|
+
self.keepalive_task = asyncio.create_task(self._keepalive_loop(), name='tigrcorn-h2-keepalive')
|
|
260
|
+
while self.running:
|
|
261
|
+
if self._should_finish_after_peer_goaway():
|
|
262
|
+
break
|
|
263
|
+
frames = self.frame_buffer.pop_all()
|
|
264
|
+
if frames:
|
|
265
|
+
for frame in frames:
|
|
266
|
+
await self._handle_frame(frame)
|
|
267
|
+
continue
|
|
268
|
+
data = await asyncio.wait_for(self.reader.read(65535), timeout=self.config.http.read_timeout)
|
|
269
|
+
if not data:
|
|
270
|
+
break
|
|
271
|
+
self.frame_buffer.feed(data)
|
|
272
|
+
finally:
|
|
273
|
+
if self.keepalive_task is not None:
|
|
274
|
+
self.keepalive_task.cancel()
|
|
275
|
+
with suppress(asyncio.CancelledError):
|
|
276
|
+
await self.keepalive_task
|
|
277
|
+
await self._shutdown_streams()
|
|
278
|
+
|
|
279
|
+
async def _ensure_preface(self) -> None:
|
|
280
|
+
if self.prebuffer == H2_PREFACE:
|
|
281
|
+
self.state.preface_seen = True
|
|
282
|
+
return
|
|
283
|
+
if self.prebuffer:
|
|
284
|
+
raise ProtocolError("unexpected HTTP/2 prebuffer state")
|
|
285
|
+
received = await self.reader.readexactly(len(H2_PREFACE))
|
|
286
|
+
if received != H2_PREFACE:
|
|
287
|
+
raise ProtocolError("invalid HTTP/2 client preface")
|
|
288
|
+
self.state.preface_seen = True
|
|
289
|
+
|
|
290
|
+
def _check_frame_header(self, frame: HTTP2Frame) -> None:
|
|
291
|
+
if frame.length > self.state.local_settings[0x5]:
|
|
292
|
+
raise ProtocolError("received HTTP/2 frame exceeds local MAX_FRAME_SIZE")
|
|
293
|
+
if not self.state.remote_settings_seen and frame.frame_type != FRAME_SETTINGS:
|
|
294
|
+
raise ProtocolError("HTTP/2 first frame after preface must be SETTINGS")
|
|
295
|
+
if self._continuation_stream_id is not None and (
|
|
296
|
+
frame.frame_type != FRAME_CONTINUATION or frame.stream_id != self._continuation_stream_id
|
|
297
|
+
):
|
|
298
|
+
raise ProtocolError("unexpected frame while CONTINUATION is pending")
|
|
299
|
+
|
|
300
|
+
async def _handle_frame(self, frame: HTTP2Frame) -> None:
|
|
301
|
+
self._check_frame_header(frame)
|
|
302
|
+
self._record_keepalive_activity()
|
|
303
|
+
if frame.frame_type == FRAME_SETTINGS:
|
|
304
|
+
await self._handle_settings(frame)
|
|
305
|
+
return
|
|
306
|
+
if frame.frame_type == FRAME_HEADERS:
|
|
307
|
+
await self._handle_headers(frame)
|
|
308
|
+
return
|
|
309
|
+
if frame.frame_type == FRAME_CONTINUATION:
|
|
310
|
+
await self._handle_continuation(frame)
|
|
311
|
+
return
|
|
312
|
+
if frame.frame_type == FRAME_DATA:
|
|
313
|
+
await self._handle_data(frame)
|
|
314
|
+
return
|
|
315
|
+
if frame.frame_type == FRAME_WINDOW_UPDATE:
|
|
316
|
+
await self._handle_window_update(frame)
|
|
317
|
+
return
|
|
318
|
+
if frame.frame_type == FRAME_PING:
|
|
319
|
+
await self._handle_ping(frame)
|
|
320
|
+
return
|
|
321
|
+
if frame.frame_type == FRAME_PRIORITY:
|
|
322
|
+
self._handle_priority(frame)
|
|
323
|
+
return
|
|
324
|
+
if frame.frame_type == FRAME_PUSH_PROMISE:
|
|
325
|
+
self._handle_push_promise(frame)
|
|
326
|
+
return
|
|
327
|
+
if frame.frame_type == FRAME_RST_STREAM:
|
|
328
|
+
await self._handle_rst_stream(frame)
|
|
329
|
+
return
|
|
330
|
+
if frame.frame_type == FRAME_GOAWAY:
|
|
331
|
+
self._handle_goaway(frame)
|
|
332
|
+
return
|
|
333
|
+
# Unknown extension frames are ignored unless a CONTINUATION sequence is pending.
|
|
334
|
+
|
|
335
|
+
async def _handle_settings(self, frame: HTTP2Frame) -> None:
|
|
336
|
+
if frame.stream_id != 0:
|
|
337
|
+
raise ProtocolError("SETTINGS must use stream 0")
|
|
338
|
+
if frame.flags & FLAG_ACK:
|
|
339
|
+
if not self.state.remote_settings_seen:
|
|
340
|
+
raise ProtocolError("HTTP/2 peer must send initial SETTINGS before ACK")
|
|
341
|
+
if frame.payload:
|
|
342
|
+
raise ProtocolError("ACK SETTINGS must have empty payload")
|
|
343
|
+
return
|
|
344
|
+
self.state.remote_settings_seen = True
|
|
345
|
+
settings = decode_settings(frame.payload)
|
|
346
|
+
if 0x1 in settings:
|
|
347
|
+
self.hpack_encoder.set_max_table_size(settings[0x1])
|
|
348
|
+
old_initial_window = self.state.remote_settings.get(0x4, DEFAULT_SETTINGS[0x4])
|
|
349
|
+
self.state.remote_settings.update(settings)
|
|
350
|
+
new_initial_window = self.state.remote_settings.get(0x4, DEFAULT_SETTINGS[0x4])
|
|
351
|
+
delta = new_initial_window - old_initial_window
|
|
352
|
+
if delta:
|
|
353
|
+
self.streams.apply_window_delta(delta)
|
|
354
|
+
if delta > 0:
|
|
355
|
+
self._notify_waiter(0)
|
|
356
|
+
self.frame_writer.max_frame_size = self.state.max_frame_size
|
|
357
|
+
await self._write_raw(serialize_settings_ack())
|
|
358
|
+
|
|
359
|
+
def _validate_new_remote_stream(self, stream_id: int) -> None:
|
|
360
|
+
if stream_id % 2 == 0:
|
|
361
|
+
raise ProtocolError("client-initiated HTTP/2 streams must use odd stream ids")
|
|
362
|
+
if stream_id <= self.state.highest_remote_stream_id:
|
|
363
|
+
raise ProtocolError("HTTP/2 stream ids must increase")
|
|
364
|
+
if self.state.peer_goaway_received or self.state.local_goaway_sent:
|
|
365
|
+
raise ProtocolError("HTTP/2 new stream received after GOAWAY")
|
|
366
|
+
if self.streams.active_remote_stream_count() >= self.state.max_concurrent_streams:
|
|
367
|
+
raise ProtocolError("HTTP/2 maximum concurrent streams exceeded")
|
|
368
|
+
self.state.highest_remote_stream_id = stream_id
|
|
369
|
+
self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
|
|
370
|
+
|
|
371
|
+
def _append_header_fragment(self, state: H2StreamState, fragment: bytes) -> None:
|
|
372
|
+
next_size = state.header_block_bytes + len(fragment)
|
|
373
|
+
if next_size > self.config.http.http2_max_headers_size:
|
|
374
|
+
raise ProtocolError("request head exceeds configured http2_max_headers_size")
|
|
375
|
+
state.header_block_bytes = next_size
|
|
376
|
+
state.header_fragments.append(fragment)
|
|
377
|
+
|
|
378
|
+
def _validate_header_list_size(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
379
|
+
size = sum(len(name) + len(value) + 32 for name, value in headers)
|
|
380
|
+
if size > self.state.max_header_list_size:
|
|
381
|
+
raise ProtocolError("HTTP/2 header list exceeds configured maximum")
|
|
382
|
+
|
|
383
|
+
def _validate_trailer_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
384
|
+
for name, value in headers:
|
|
385
|
+
if any(65 <= byte <= 90 for byte in name):
|
|
386
|
+
raise ProtocolError("uppercase header field name forbidden in HTTP/2")
|
|
387
|
+
if name.startswith(b":"):
|
|
388
|
+
raise ProtocolError("trailer pseudo-header forbidden in HTTP/2")
|
|
389
|
+
if name in {b"connection", b"upgrade", b"proxy-connection", b"transfer-encoding"}:
|
|
390
|
+
raise ProtocolError("connection-specific header forbidden in HTTP/2")
|
|
391
|
+
if name == b"te" and value.lower() != b"trailers":
|
|
392
|
+
raise ProtocolError("invalid TE header for HTTP/2")
|
|
393
|
+
|
|
394
|
+
async def _handle_headers(self, frame: HTTP2Frame) -> None:
|
|
395
|
+
if frame.stream_id == 0:
|
|
396
|
+
raise ProtocolError("HEADERS must use a stream id")
|
|
397
|
+
if self._continuation_stream_id not in (None, frame.stream_id):
|
|
398
|
+
raise ProtocolError("unexpected HEADERS while CONTINUATION is pending")
|
|
399
|
+
state = self.streams.find(frame.stream_id)
|
|
400
|
+
is_new_stream = state is None
|
|
401
|
+
if is_new_stream:
|
|
402
|
+
if self.streams.is_closed(frame.stream_id):
|
|
403
|
+
raise ProtocolError("HEADERS on closed HTTP/2 stream")
|
|
404
|
+
self._validate_new_remote_stream(frame.stream_id)
|
|
405
|
+
state = self.streams.activate_remote(
|
|
406
|
+
frame.stream_id,
|
|
407
|
+
send_window=self.state.initial_window_size,
|
|
408
|
+
receive_window=self.state.local_initial_window_size,
|
|
409
|
+
)
|
|
410
|
+
state.current_header_block_is_trailers = False
|
|
411
|
+
state.open_remote(end_stream=bool(frame.flags & FLAG_END_STREAM))
|
|
412
|
+
else:
|
|
413
|
+
if state.closed:
|
|
414
|
+
raise ProtocolError("HEADERS on closed HTTP/2 stream")
|
|
415
|
+
if not state.headers_complete:
|
|
416
|
+
raise ProtocolError("duplicate HTTP/2 initial HEADERS block")
|
|
417
|
+
if state.awaiting_continuation:
|
|
418
|
+
raise ProtocolError("unexpected HEADERS while CONTINUATION is pending")
|
|
419
|
+
if state.lifecycle not in {H2StreamLifecycle.OPEN, H2StreamLifecycle.HALF_CLOSED_LOCAL}:
|
|
420
|
+
raise ProtocolError("HEADERS not permitted in current HTTP/2 stream state")
|
|
421
|
+
if state.end_stream_received or state.trailers_complete:
|
|
422
|
+
raise ProtocolError("trailing HEADERS not permitted after end of stream")
|
|
423
|
+
if not (frame.flags & FLAG_END_STREAM):
|
|
424
|
+
raise ProtocolError("trailing HTTP/2 HEADERS must carry END_STREAM")
|
|
425
|
+
state.current_header_block_is_trailers = True
|
|
426
|
+
state.receive_end_stream()
|
|
427
|
+
self._append_header_fragment(state, headers_payload_fragment(frame.payload, frame.flags))
|
|
428
|
+
state.awaiting_continuation = not bool(frame.flags & FLAG_END_HEADERS)
|
|
429
|
+
if state.awaiting_continuation:
|
|
430
|
+
self._continuation_stream_id = frame.stream_id
|
|
431
|
+
return
|
|
432
|
+
self._continuation_stream_id = None
|
|
433
|
+
self._finish_headers(state)
|
|
434
|
+
await self._maybe_dispatch(frame.stream_id)
|
|
435
|
+
|
|
436
|
+
async def _handle_continuation(self, frame: HTTP2Frame) -> None:
|
|
437
|
+
if frame.stream_id == 0:
|
|
438
|
+
raise ProtocolError("CONTINUATION must use a stream id")
|
|
439
|
+
if self._continuation_stream_id != frame.stream_id:
|
|
440
|
+
raise ProtocolError("unexpected CONTINUATION stream")
|
|
441
|
+
state = self.streams.find(frame.stream_id)
|
|
442
|
+
if state is None:
|
|
443
|
+
raise ProtocolError("CONTINUATION for unknown stream")
|
|
444
|
+
self._append_header_fragment(state, frame.payload)
|
|
445
|
+
state.awaiting_continuation = not bool(frame.flags & FLAG_END_HEADERS)
|
|
446
|
+
if state.awaiting_continuation:
|
|
447
|
+
return
|
|
448
|
+
self._continuation_stream_id = None
|
|
449
|
+
self._finish_headers(state)
|
|
450
|
+
await self._maybe_dispatch(frame.stream_id)
|
|
451
|
+
|
|
452
|
+
async def _consume_receive_flow(self, stream_id: int, amount: int) -> None:
|
|
453
|
+
if amount <= 0:
|
|
454
|
+
return
|
|
455
|
+
self.state.connection_receive_window.consume(amount)
|
|
456
|
+
if self.state.connection_receive_window.available < 0:
|
|
457
|
+
raise ProtocolError("HTTP/2 connection flow-control window exceeded")
|
|
458
|
+
state = self.streams.find(stream_id)
|
|
459
|
+
if state is None:
|
|
460
|
+
raise ProtocolError("HTTP/2 stream flow-control used after closure")
|
|
461
|
+
state.receive_window.consume(amount)
|
|
462
|
+
if state.receive_window.available < 0:
|
|
463
|
+
raise ProtocolError("HTTP/2 stream flow-control window exceeded")
|
|
464
|
+
|
|
465
|
+
async def _maybe_replenish_receive_credit(self, stream_id: int, amount: int) -> None:
|
|
466
|
+
if amount <= 0:
|
|
467
|
+
return
|
|
468
|
+
updates: list[bytes] = []
|
|
469
|
+
self.state.connection_receive_consumed_since_update += amount
|
|
470
|
+
connection_increment = 0
|
|
471
|
+
if self.config.http.http2_adaptive_window:
|
|
472
|
+
new_connection_target = next_adaptive_window_target(
|
|
473
|
+
self.state.connection_receive_window_target,
|
|
474
|
+
max(amount, self.state.connection_receive_consumed_since_update),
|
|
475
|
+
)
|
|
476
|
+
if new_connection_target > self.state.connection_receive_window_target:
|
|
477
|
+
delta_target = new_connection_target - self.state.connection_receive_window_target
|
|
478
|
+
self.state.connection_receive_window_target = new_connection_target
|
|
479
|
+
self.state.connection_receive_window.increase(delta_target)
|
|
480
|
+
connection_increment += delta_target
|
|
481
|
+
connection_threshold = max(1, self.state.connection_receive_window_target // 2)
|
|
482
|
+
if (
|
|
483
|
+
self.state.connection_receive_window.available <= connection_threshold
|
|
484
|
+
or self.state.connection_receive_consumed_since_update >= connection_threshold
|
|
485
|
+
):
|
|
486
|
+
increment = self.state.connection_receive_consumed_since_update
|
|
487
|
+
self.state.connection_receive_consumed_since_update = 0
|
|
488
|
+
self.state.connection_receive_window.increase(increment)
|
|
489
|
+
connection_increment += increment
|
|
490
|
+
if connection_increment > 0:
|
|
491
|
+
updates.append(serialize_window_update(0, connection_increment))
|
|
492
|
+
state = self.streams.find(stream_id)
|
|
493
|
+
if state is None:
|
|
494
|
+
for update in updates:
|
|
495
|
+
await self._write_raw(update)
|
|
496
|
+
return
|
|
497
|
+
state.receive_consumed_since_update += amount
|
|
498
|
+
stream_increment = 0
|
|
499
|
+
if self.config.http.http2_adaptive_window:
|
|
500
|
+
new_stream_target = next_adaptive_window_target(
|
|
501
|
+
state.receive_window_target,
|
|
502
|
+
max(amount, state.receive_consumed_since_update),
|
|
503
|
+
)
|
|
504
|
+
if new_stream_target > state.receive_window_target:
|
|
505
|
+
delta_target = new_stream_target - state.receive_window_target
|
|
506
|
+
state.receive_window_target = new_stream_target
|
|
507
|
+
state.receive_window.increase(delta_target)
|
|
508
|
+
stream_increment += delta_target
|
|
509
|
+
stream_threshold = max(1, state.receive_window_target // 2)
|
|
510
|
+
if state.receive_window.available <= stream_threshold or state.receive_consumed_since_update >= stream_threshold:
|
|
511
|
+
increment = state.receive_consumed_since_update
|
|
512
|
+
state.receive_consumed_since_update = 0
|
|
513
|
+
state.receive_window.increase(increment)
|
|
514
|
+
stream_increment += increment
|
|
515
|
+
if stream_increment > 0:
|
|
516
|
+
updates.append(serialize_window_update(stream_id, stream_increment))
|
|
517
|
+
for update in updates:
|
|
518
|
+
await self._write_raw(update)
|
|
519
|
+
|
|
520
|
+
async def _handle_data(self, frame: HTTP2Frame) -> None:
|
|
521
|
+
if frame.stream_id == 0:
|
|
522
|
+
raise ProtocolError("DATA must use a stream id")
|
|
523
|
+
if self.streams.is_closed(frame.stream_id):
|
|
524
|
+
return
|
|
525
|
+
state = self.streams.find(frame.stream_id)
|
|
526
|
+
if state is None:
|
|
527
|
+
raise ProtocolError("DATA on idle HTTP/2 stream")
|
|
528
|
+
if state.awaiting_continuation:
|
|
529
|
+
raise ProtocolError("DATA received before END_HEADERS")
|
|
530
|
+
if not state.headers_complete:
|
|
531
|
+
raise ProtocolError("DATA before HEADERS")
|
|
532
|
+
if state.trailers_complete or state.end_stream_received or state.closed:
|
|
533
|
+
raise ProtocolError("DATA on half-closed HTTP/2 stream")
|
|
534
|
+
payload = strip_padding(frame.payload, frame.flags)
|
|
535
|
+
await self._consume_receive_flow(frame.stream_id, len(payload))
|
|
536
|
+
if state.websocket_session is not None:
|
|
537
|
+
await state.websocket_session.feed_data(payload, end_stream=bool(frame.flags & FLAG_END_STREAM))
|
|
538
|
+
elif state.connect_tunnel is not None:
|
|
539
|
+
await state.connect_tunnel.feed_client_data(payload, end_stream=bool(frame.flags & FLAG_END_STREAM))
|
|
540
|
+
elif payload:
|
|
541
|
+
if state.buffered_body_size + len(payload) > self.config.max_body_size:
|
|
542
|
+
raise ProtocolError("request body exceeds configured max_body_size")
|
|
543
|
+
state.append_body(payload)
|
|
544
|
+
await self._maybe_replenish_receive_credit(frame.stream_id, len(payload))
|
|
545
|
+
if frame.flags & FLAG_END_STREAM:
|
|
546
|
+
state.receive_end_stream()
|
|
547
|
+
await self._maybe_dispatch(frame.stream_id)
|
|
548
|
+
self._finalize_stream_if_complete(frame.stream_id)
|
|
549
|
+
|
|
550
|
+
def _finish_headers(self, state: H2StreamState) -> None:
|
|
551
|
+
block = b"".join(state.header_fragments)
|
|
552
|
+
headers = self.hpack_decoder.decode_header_block(block)
|
|
553
|
+
self._validate_header_list_size(headers)
|
|
554
|
+
if state.current_header_block_is_trailers:
|
|
555
|
+
self._validate_trailer_headers(headers)
|
|
556
|
+
state.trailers = headers
|
|
557
|
+
state.trailers_complete = True
|
|
558
|
+
else:
|
|
559
|
+
state.headers = headers
|
|
560
|
+
state.headers_complete = True
|
|
561
|
+
state.header_fragments.clear()
|
|
562
|
+
state.header_block_bytes = 0
|
|
563
|
+
state.awaiting_continuation = False
|
|
564
|
+
state.current_header_block_is_trailers = False
|
|
565
|
+
|
|
566
|
+
async def _maybe_dispatch(self, stream_id: int) -> None:
|
|
567
|
+
state = self.streams.find(stream_id)
|
|
568
|
+
if state is None or state.dispatched or not state.headers_complete:
|
|
569
|
+
return
|
|
570
|
+
is_ws = self._is_extended_connect_websocket(state.headers)
|
|
571
|
+
is_connect = self._is_generic_connect_tunnel(state.headers)
|
|
572
|
+
if not is_ws and not is_connect and not state.end_stream_received:
|
|
573
|
+
return
|
|
574
|
+
if not self._admit_stream_work(stream_id):
|
|
575
|
+
request = self._build_request(state)
|
|
576
|
+
await self._send_response(stream_id, 503, [(b"content-type", b"text/plain")], b"scheduler overloaded")
|
|
577
|
+
self.access_logger.log_http(self.client, request.method, request.path, 503, "HTTP/2")
|
|
578
|
+
self._release_stream_work_lease(stream_id)
|
|
579
|
+
self._cancel_stream(stream_id)
|
|
580
|
+
self.streams.close(stream_id)
|
|
581
|
+
self._maybe_finish_after_goaway()
|
|
582
|
+
return
|
|
583
|
+
state.dispatched = True
|
|
584
|
+
if is_ws:
|
|
585
|
+
await self._start_websocket_stream(stream_id)
|
|
586
|
+
return
|
|
587
|
+
if is_connect:
|
|
588
|
+
await self._start_connect_tunnel(stream_id)
|
|
589
|
+
return
|
|
590
|
+
self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
|
|
591
|
+
task = asyncio.create_task(self._run_stream(stream_id), name=f"tigrcorn-h2-stream-{stream_id}")
|
|
592
|
+
self.stream_tasks[stream_id] = task
|
|
593
|
+
|
|
594
|
+
async def _handle_window_update(self, frame: HTTP2Frame) -> None:
|
|
595
|
+
increment = parse_window_update(frame.payload)
|
|
596
|
+
if frame.stream_id == 0:
|
|
597
|
+
self.state.connection_send_window.increase(increment)
|
|
598
|
+
self._notify_waiter(0)
|
|
599
|
+
return
|
|
600
|
+
if self.streams.is_closed(frame.stream_id):
|
|
601
|
+
return
|
|
602
|
+
state = self.streams.find(frame.stream_id)
|
|
603
|
+
if state is None:
|
|
604
|
+
raise ProtocolError("WINDOW_UPDATE on idle HTTP/2 stream")
|
|
605
|
+
state.send_window.increase(increment)
|
|
606
|
+
self._notify_waiter(frame.stream_id)
|
|
607
|
+
|
|
608
|
+
async def _handle_ping(self, frame: HTTP2Frame) -> None:
|
|
609
|
+
if frame.stream_id != 0:
|
|
610
|
+
raise ProtocolError("PING must use stream 0")
|
|
611
|
+
if len(frame.payload) != 8:
|
|
612
|
+
raise ProtocolError("PING payload must be 8 bytes")
|
|
613
|
+
if frame.flags & FLAG_ACK:
|
|
614
|
+
if self.keepalive is not None:
|
|
615
|
+
self.keepalive.acknowledge_pong(frame.payload)
|
|
616
|
+
return
|
|
617
|
+
await self._write_raw(serialize_ping(frame.payload, ack=True))
|
|
618
|
+
|
|
619
|
+
def _handle_priority(self, frame: HTTP2Frame) -> None:
|
|
620
|
+
if frame.stream_id == 0:
|
|
621
|
+
raise ProtocolError("PRIORITY must use a stream id")
|
|
622
|
+
_exclusive, dependency, _weight = parse_priority(frame.payload)
|
|
623
|
+
if dependency == frame.stream_id:
|
|
624
|
+
raise ProtocolError("HTTP/2 PRIORITY stream dependency cannot depend on itself")
|
|
625
|
+
|
|
626
|
+
def _handle_push_promise(self, frame: HTTP2Frame) -> None:
|
|
627
|
+
if frame.stream_id == 0:
|
|
628
|
+
raise ProtocolError("PUSH_PROMISE must use a stream id")
|
|
629
|
+
raise ProtocolError("clients must not send PUSH_PROMISE to an HTTP/2 server")
|
|
630
|
+
|
|
631
|
+
async def _handle_rst_stream(self, frame: HTTP2Frame) -> None:
|
|
632
|
+
if frame.stream_id == 0 or len(frame.payload) != 4:
|
|
633
|
+
raise ProtocolError("invalid RST_STREAM frame")
|
|
634
|
+
if self.streams.is_closed(frame.stream_id):
|
|
635
|
+
return
|
|
636
|
+
state = self.streams.find(frame.stream_id)
|
|
637
|
+
if state is None or (not state.opened and not state.reserved_local and not state.reserved_remote):
|
|
638
|
+
raise ProtocolError("RST_STREAM on idle HTTP/2 stream")
|
|
639
|
+
if state.websocket_session is not None:
|
|
640
|
+
await state.websocket_session.abort()
|
|
641
|
+
if state.connect_tunnel is not None:
|
|
642
|
+
await state.connect_tunnel.abort()
|
|
643
|
+
self._cancel_stream(frame.stream_id)
|
|
644
|
+
state.mark_reset_received()
|
|
645
|
+
self.streams.close(frame.stream_id)
|
|
646
|
+
self._notify_waiter(frame.stream_id)
|
|
647
|
+
self._maybe_finish_after_goaway()
|
|
648
|
+
|
|
649
|
+
def _handle_goaway(self, frame: HTTP2Frame) -> None:
|
|
650
|
+
if frame.stream_id != 0:
|
|
651
|
+
raise ProtocolError("GOAWAY must use stream 0")
|
|
652
|
+
last_stream_id, _error_code, _debug_data = parse_goaway(frame.payload)
|
|
653
|
+
if self.state.peer_goaway_received and self.state.peer_last_stream_id is not None:
|
|
654
|
+
if last_stream_id > self.state.peer_last_stream_id:
|
|
655
|
+
raise ProtocolError("HTTP/2 GOAWAY last_stream_id must not increase")
|
|
656
|
+
self.state.peer_goaway_received = True
|
|
657
|
+
self.state.peer_last_stream_id = last_stream_id
|
|
658
|
+
self.state.shutdown = True
|
|
659
|
+
self._maybe_finish_after_goaway()
|
|
660
|
+
|
|
661
|
+
def _should_finish_after_peer_goaway(self) -> bool:
|
|
662
|
+
return (
|
|
663
|
+
self.state.peer_goaway_received
|
|
664
|
+
and self._continuation_stream_id is None
|
|
665
|
+
and not self.streams.streams
|
|
666
|
+
and not self.stream_tasks
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
def _maybe_finish_after_goaway(self) -> None:
|
|
670
|
+
if self._should_finish_after_peer_goaway():
|
|
671
|
+
self.running = False
|
|
672
|
+
|
|
673
|
+
def _pseudo_headers(self, headers: list[tuple[bytes, bytes]]) -> dict[bytes, bytes]:
|
|
674
|
+
return {k: v for k, v in headers if k.startswith(b":")}
|
|
675
|
+
|
|
676
|
+
def _is_extended_connect_websocket(self, headers: list[tuple[bytes, bytes]]) -> bool:
|
|
677
|
+
pseudo = self._pseudo_headers(headers)
|
|
678
|
+
return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") == b"websocket"
|
|
679
|
+
|
|
680
|
+
def _is_generic_connect_tunnel(self, headers: list[tuple[bytes, bytes]]) -> bool:
|
|
681
|
+
pseudo = self._pseudo_headers(headers)
|
|
682
|
+
return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") is None
|
|
683
|
+
def _release_stream_work_lease(self, stream_id: int) -> None:
|
|
684
|
+
lease = self.stream_work_leases.pop(stream_id, None)
|
|
685
|
+
if lease is not None:
|
|
686
|
+
lease.release()
|
|
687
|
+
|
|
688
|
+
def _on_websocket_stream_closed(self, stream_id: int) -> None:
|
|
689
|
+
state = self.streams.find(stream_id)
|
|
690
|
+
if state is not None:
|
|
691
|
+
state.websocket_session = None
|
|
692
|
+
self._release_stream_work_lease(stream_id)
|
|
693
|
+
self._finalize_stream_if_complete(stream_id)
|
|
694
|
+
|
|
695
|
+
def _admit_stream_work(self, stream_id: int) -> bool:
|
|
696
|
+
if self.scheduler is None:
|
|
697
|
+
return True
|
|
698
|
+
lease = self.scheduler.acquire_work()
|
|
699
|
+
if lease is None:
|
|
700
|
+
if self.metrics is not None:
|
|
701
|
+
self.metrics.scheduler_task_rejected()
|
|
702
|
+
return False
|
|
703
|
+
self.stream_work_leases[stream_id] = lease
|
|
704
|
+
return True
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _next_local_push_stream_id(self) -> int:
|
|
708
|
+
max_local_streams = self.state.remote_settings.get(0x3)
|
|
709
|
+
if max_local_streams is not None and self.streams.active_local_stream_count() >= max_local_streams:
|
|
710
|
+
raise ProtocolError("HTTP/2 peer refused additional server-initiated streams")
|
|
711
|
+
stream_id = self.state.next_local_stream_id
|
|
712
|
+
while self.streams.find(stream_id) is not None or self.streams.is_closed(stream_id):
|
|
713
|
+
stream_id += 2
|
|
714
|
+
if stream_id > 0x7FFFFFFF:
|
|
715
|
+
raise ProtocolError("exhausted HTTP/2 server-initiated stream identifiers")
|
|
716
|
+
self.state.next_local_stream_id = stream_id + 2
|
|
717
|
+
return stream_id
|
|
718
|
+
|
|
719
|
+
def _build_push_request(self, parent_stream_id: int, message: dict) -> ParsedRequest:
|
|
720
|
+
state = self.streams.find(parent_stream_id)
|
|
721
|
+
if state is None:
|
|
722
|
+
raise ProtocolError("cannot create HTTP/2 server push from an unknown stream")
|
|
723
|
+
if self._is_extended_connect_websocket(state.headers) or self._is_generic_connect_tunnel(state.headers):
|
|
724
|
+
raise ProtocolError("HTTP/2 server push is not available on CONNECT streams")
|
|
725
|
+
pseudo = self._pseudo_headers(state.headers)
|
|
726
|
+
path = message.get("path")
|
|
727
|
+
if not path:
|
|
728
|
+
raise ProtocolError("http.response.push requires a path")
|
|
729
|
+
if isinstance(path, bytes):
|
|
730
|
+
target = path.decode("ascii", "strict")
|
|
731
|
+
else:
|
|
732
|
+
target = str(path)
|
|
733
|
+
method = message.get("method", "GET")
|
|
734
|
+
if isinstance(method, bytes):
|
|
735
|
+
method_text = method.decode("ascii", "strict").upper()
|
|
736
|
+
else:
|
|
737
|
+
method_text = str(method).upper()
|
|
738
|
+
authority = message.get("authority")
|
|
739
|
+
if authority is None:
|
|
740
|
+
authority_bytes = pseudo.get(b":authority", b"")
|
|
741
|
+
elif isinstance(authority, bytes):
|
|
742
|
+
authority_bytes = authority
|
|
743
|
+
else:
|
|
744
|
+
authority_bytes = str(authority).encode("ascii", "strict")
|
|
745
|
+
scheme = message.get("scheme")
|
|
746
|
+
if scheme is None:
|
|
747
|
+
scheme_bytes = pseudo.get(b":scheme", self.scheme.encode("ascii"))
|
|
748
|
+
elif isinstance(scheme, bytes):
|
|
749
|
+
scheme_bytes = scheme
|
|
750
|
+
else:
|
|
751
|
+
scheme_bytes = str(scheme).encode("ascii", "strict")
|
|
752
|
+
extra_headers = [
|
|
753
|
+
(bytes(name).lower(), bytes(value))
|
|
754
|
+
for name, value in message.get("headers", [])
|
|
755
|
+
if not bytes(name).startswith(b":")
|
|
756
|
+
]
|
|
757
|
+
split = urlsplit(target)
|
|
758
|
+
path_text = split.path or "/"
|
|
759
|
+
raw_path = path_text.encode("utf-8")
|
|
760
|
+
query_string = split.query.encode("ascii")
|
|
761
|
+
pseudo_headers = [
|
|
762
|
+
(b":method", method_text.encode("ascii")),
|
|
763
|
+
(b":path", target.encode("utf-8")),
|
|
764
|
+
(b":scheme", scheme_bytes),
|
|
765
|
+
(b":authority", authority_bytes),
|
|
766
|
+
]
|
|
767
|
+
return ParsedRequest(
|
|
768
|
+
method=method_text,
|
|
769
|
+
target=target,
|
|
770
|
+
path=path_text,
|
|
771
|
+
raw_path=raw_path,
|
|
772
|
+
query_string=query_string,
|
|
773
|
+
http_version="2",
|
|
774
|
+
headers=extra_headers,
|
|
775
|
+
body=b"",
|
|
776
|
+
keep_alive=True,
|
|
777
|
+
expect_continue=False,
|
|
778
|
+
websocket_upgrade=False,
|
|
779
|
+
), pseudo_headers + extra_headers
|
|
780
|
+
|
|
781
|
+
async def _run_http_app(self, stream_id: int, request: ParsedRequest, *, allow_push: bool) -> tuple[int, list[tuple[bytes, bytes]], bytes, list[tuple[bytes, bytes]], list[tuple[int, list[tuple[bytes, bytes]]]], list | None, object | None]:
|
|
782
|
+
extensions = dict(self.scope_extensions)
|
|
783
|
+
state = self.streams.find(stream_id)
|
|
784
|
+
raw_request_trailers = list(state.trailers) if state is not None else []
|
|
785
|
+
try:
|
|
786
|
+
request_trailers = apply_request_trailer_policy(raw_request_trailers, self.config.http.trailer_policy)
|
|
787
|
+
except ProtocolError:
|
|
788
|
+
return 400, [(b"content-type", b"text/plain")], b"bad request trailers", [], [], None, None
|
|
789
|
+
if request.method.upper() == "CONNECT":
|
|
790
|
+
extensions["tigrcorn.http.connect"] = {"authority": request.target}
|
|
791
|
+
if request_trailers and self.config.http.trailer_policy != 'drop':
|
|
792
|
+
extensions["tigrcorn.http.request_trailers"] = {}
|
|
793
|
+
if allow_push and self.state.client_allows_push:
|
|
794
|
+
extensions["http.response.push"] = {}
|
|
795
|
+
extensions['tigrcorn.http.response.file'] = {'protocol': 'http/2', 'streaming': True, 'sendfile': False}
|
|
796
|
+
extensions['http.response.pathsend'] = {}
|
|
797
|
+
scope = build_http_scope(request, client=self.client, server=self.server, scheme=self.scheme, extensions=extensions, root_path=self.config.proxy.root_path, proxy=self.config.proxy)
|
|
798
|
+
receive = HTTPRequestReceive(request.body, trailers=request_trailers, trailer_policy=self.config.http.trailer_policy)
|
|
799
|
+
collector = HTTPResponseCollector()
|
|
800
|
+
|
|
801
|
+
async def send(message: dict) -> None:
|
|
802
|
+
if message.get("type") == "http.response.push":
|
|
803
|
+
if not allow_push or not self.state.client_allows_push:
|
|
804
|
+
raise ProtocolError("HTTP/2 server push is not available on this stream")
|
|
805
|
+
await self._send_push_promise(stream_id, message)
|
|
806
|
+
return
|
|
807
|
+
await collector(message)
|
|
808
|
+
|
|
809
|
+
status = 500
|
|
810
|
+
cleanup: object | None = None
|
|
811
|
+
try:
|
|
812
|
+
await self.app(scope, receive, send)
|
|
813
|
+
collector.finalize()
|
|
814
|
+
assert collector.status is not None
|
|
815
|
+
status = collector.status
|
|
816
|
+
headers = list(collector.headers)
|
|
817
|
+
trailers = list(collector.trailers)
|
|
818
|
+
informational = list(collector.informational_responses)
|
|
819
|
+
body_segments = list(collector.body_segments) if collector.uses_streamed_body else None
|
|
820
|
+
if body_segments is not None:
|
|
821
|
+
cleanup = collector.cleanup if collector.has_spooled_body() else None
|
|
822
|
+
return status, headers, b'', trailers, informational, body_segments, cleanup
|
|
823
|
+
if collector.has_spooled_body():
|
|
824
|
+
spooled_segments = collector.spooled_body_segments()
|
|
825
|
+
spooled_path = ''
|
|
826
|
+
if spooled_segments:
|
|
827
|
+
first_segment = spooled_segments[0]
|
|
828
|
+
spooled_path = getattr(first_segment, 'path', '')
|
|
829
|
+
planned = plan_file_backed_response_entity_semantics(
|
|
830
|
+
method=request.method,
|
|
831
|
+
request_headers=request.headers,
|
|
832
|
+
response_headers=headers,
|
|
833
|
+
status=status,
|
|
834
|
+
body_path=spooled_path,
|
|
835
|
+
body_length=collector.body_length,
|
|
836
|
+
generated_etag=collector.generated_entity_tag(),
|
|
837
|
+
apply_content_coding=True,
|
|
838
|
+
trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
|
|
839
|
+
)
|
|
840
|
+
cleanup = collector.cleanup
|
|
841
|
+
if planned.requires_materialization:
|
|
842
|
+
body = await collector.materialize_body()
|
|
843
|
+
processed = apply_response_entity_semantics(
|
|
844
|
+
method=request.method,
|
|
845
|
+
request_headers=request.headers,
|
|
846
|
+
response_headers=headers,
|
|
847
|
+
body=body,
|
|
848
|
+
status=status,
|
|
849
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
850
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
851
|
+
apply_content_coding=True,
|
|
852
|
+
generate_etag=True,
|
|
853
|
+
)
|
|
854
|
+
return processed.status, processed.headers, processed.body, ([] if processed.head_response else trailers), informational, None, cleanup
|
|
855
|
+
if planned.use_body_segments:
|
|
856
|
+
return planned.status, planned.headers, b'', trailers, informational, list(planned.body_segments), cleanup
|
|
857
|
+
return planned.status, planned.headers, planned.body, [], informational, None, cleanup
|
|
858
|
+
body = await collector.materialize_body()
|
|
859
|
+
except Exception:
|
|
860
|
+
collector.cleanup()
|
|
861
|
+
status, headers, body, trailers = 500, [(b"content-type", b"text/plain")], b"internal server error", []
|
|
862
|
+
informational = []
|
|
863
|
+
body_segments = None
|
|
864
|
+
cleanup = None
|
|
865
|
+
processed = apply_response_entity_semantics(
|
|
866
|
+
method=request.method,
|
|
867
|
+
request_headers=request.headers,
|
|
868
|
+
response_headers=headers,
|
|
869
|
+
body=body,
|
|
870
|
+
status=status,
|
|
871
|
+
content_coding_policy=self.config.http.content_coding_policy,
|
|
872
|
+
supported_codings=tuple(self.config.http.content_codings),
|
|
873
|
+
apply_content_coding=True,
|
|
874
|
+
generate_etag=True,
|
|
875
|
+
)
|
|
876
|
+
return processed.status, processed.headers, processed.body, ([] if processed.head_response else trailers), informational, None, cleanup
|
|
877
|
+
|
|
878
|
+
async def _send_push_promise(self, parent_stream_id: int, message: dict) -> None:
|
|
879
|
+
if not self.state.client_allows_push:
|
|
880
|
+
return
|
|
881
|
+
promised_stream_id = self._next_local_push_stream_id()
|
|
882
|
+
request, request_headers = self._build_push_request(parent_stream_id, message)
|
|
883
|
+
header_block = self.hpack_encoder.encode_header_block(request_headers)
|
|
884
|
+
await self._write_raw(self.frame_writer.push_promise(parent_stream_id, promised_stream_id, header_block))
|
|
885
|
+
self.streams.reserve_local(
|
|
886
|
+
promised_stream_id,
|
|
887
|
+
send_window=self.state.initial_window_size,
|
|
888
|
+
receive_window=self.state.local_initial_window_size,
|
|
889
|
+
)
|
|
890
|
+
self.state.last_stream_id = max(self.state.last_stream_id, promised_stream_id)
|
|
891
|
+
status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(promised_stream_id, request, allow_push=False)
|
|
892
|
+
for interim_status, interim_headers in informational:
|
|
893
|
+
await self._send_stream_headers(promised_stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
|
|
894
|
+
try:
|
|
895
|
+
await self._send_response(promised_stream_id, status, headers, body, trailers, body_segments=body_segments)
|
|
896
|
+
finally:
|
|
897
|
+
if cleanup is not None:
|
|
898
|
+
cleanup()
|
|
899
|
+
if self.streams.find(promised_stream_id) is not None:
|
|
900
|
+
self._cancel_stream(promised_stream_id)
|
|
901
|
+
self.streams.close(promised_stream_id)
|
|
902
|
+
|
|
903
|
+
def _finalize_stream_if_complete(self, stream_id: int) -> None:
|
|
904
|
+
state = self.streams.find(stream_id)
|
|
905
|
+
if state is None or state.websocket_session is not None or state.connect_tunnel is not None:
|
|
906
|
+
return
|
|
907
|
+
if state.local_closed and state.end_stream_received:
|
|
908
|
+
self._release_stream_work_lease(stream_id)
|
|
909
|
+
self._cancel_stream(stream_id)
|
|
910
|
+
self.streams.close(stream_id)
|
|
911
|
+
self._maybe_finish_after_goaway()
|
|
912
|
+
|
|
913
|
+
async def _reset_connect_stream(self, stream_id: int) -> None:
|
|
914
|
+
state = self.streams.find(stream_id)
|
|
915
|
+
if state is None or state.closed:
|
|
916
|
+
return
|
|
917
|
+
if not state.reset_sent:
|
|
918
|
+
with suppress(Exception):
|
|
919
|
+
await self._write_raw(serialize_rst_stream(stream_id, H2_CONNECT_ERROR))
|
|
920
|
+
state.mark_reset_sent()
|
|
921
|
+
self._cancel_stream(stream_id)
|
|
922
|
+
self.streams.close(stream_id)
|
|
923
|
+
self._maybe_finish_after_goaway()
|
|
924
|
+
|
|
925
|
+
async def _send_stream_data(self, stream_id: int, data: bytes, *, end_stream: bool = False) -> None:
|
|
926
|
+
state = self.streams.find(stream_id)
|
|
927
|
+
if state is None or state.closed:
|
|
928
|
+
raise ProtocolError("attempted to send DATA on a closed HTTP/2 stream")
|
|
929
|
+
if not data and not end_stream:
|
|
930
|
+
return
|
|
931
|
+
if not data:
|
|
932
|
+
await self._write_raw(self.frame_writer.data(stream_id, b"", end_stream=True))
|
|
933
|
+
state.send_end_stream()
|
|
934
|
+
return
|
|
935
|
+
offset = 0
|
|
936
|
+
while offset < len(data):
|
|
937
|
+
chunk_size = min(self.state.max_frame_size, len(data) - offset)
|
|
938
|
+
while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
|
|
939
|
+
await self._wait_for_credit(stream_id)
|
|
940
|
+
allowed = min(chunk_size, self.state.connection_send_window.available, state.send_window.available)
|
|
941
|
+
if allowed <= 0:
|
|
942
|
+
await self._wait_for_credit(stream_id)
|
|
943
|
+
continue
|
|
944
|
+
chunk = data[offset : offset + allowed]
|
|
945
|
+
offset += len(chunk)
|
|
946
|
+
self.state.connection_send_window.consume(len(chunk))
|
|
947
|
+
state.send_window.consume(len(chunk))
|
|
948
|
+
final_chunk = end_stream and offset == len(data)
|
|
949
|
+
await self._write_raw(self.frame_writer.data(stream_id, chunk, end_stream=final_chunk))
|
|
950
|
+
if final_chunk:
|
|
951
|
+
state.send_end_stream()
|
|
952
|
+
|
|
953
|
+
async def _send_stream_headers(
|
|
954
|
+
self,
|
|
955
|
+
stream_id: int,
|
|
956
|
+
status: int,
|
|
957
|
+
headers: list[tuple[bytes, bytes]],
|
|
958
|
+
end_stream: bool,
|
|
959
|
+
) -> None:
|
|
960
|
+
state = self.streams.find(stream_id)
|
|
961
|
+
if state is None or state.closed:
|
|
962
|
+
raise ProtocolError("attempted to send HEADERS on a closed HTTP/2 stream")
|
|
963
|
+
normalized_headers = sanitize_early_hints_headers(headers) if status == 103 else strip_connection_specific_headers(headers)
|
|
964
|
+
policy_headers = apply_response_header_policy(
|
|
965
|
+
normalized_headers,
|
|
966
|
+
server_header=self.config.server_header_value,
|
|
967
|
+
include_date_header=self.config.include_date_header,
|
|
968
|
+
default_headers=self.config.default_response_headers,
|
|
969
|
+
alt_svc_values=() if status < 200 else configured_alt_svc_values(self.config, request_http_version='2'),
|
|
970
|
+
)
|
|
971
|
+
header_block = self.hpack_encoder.encode_header_block([(b":status", str(status).encode("ascii")), *policy_headers])
|
|
972
|
+
await self._write_raw(self.frame_writer.headers(stream_id, header_block, end_stream=end_stream))
|
|
973
|
+
if end_stream:
|
|
974
|
+
state.send_end_stream()
|
|
975
|
+
|
|
976
|
+
async def _start_connect_tunnel(self, stream_id: int) -> None:
|
|
977
|
+
state = self.streams.find(stream_id)
|
|
978
|
+
if state is None:
|
|
979
|
+
raise ProtocolError("connect stream disappeared before dispatch")
|
|
980
|
+
request = self._build_request(state)
|
|
981
|
+
try:
|
|
982
|
+
host, port = parse_connect_authority(request.target)
|
|
983
|
+
except Exception:
|
|
984
|
+
await self._send_response(stream_id, 400, [(b"content-type", b"text/plain")], b"bad connect target")
|
|
985
|
+
self.access_logger.log_http(self.client, "CONNECT", request.target, 400, "HTTP/2")
|
|
986
|
+
self._release_stream_work_lease(stream_id)
|
|
987
|
+
self._cancel_stream(stream_id)
|
|
988
|
+
self.streams.close(stream_id)
|
|
989
|
+
self._maybe_finish_after_goaway()
|
|
990
|
+
return
|
|
991
|
+
if self.config.http.connect_policy == 'deny':
|
|
992
|
+
await self._send_response(stream_id, 403, [(b"content-type", b"text/plain")], b"connect denied")
|
|
993
|
+
self.access_logger.log_http(self.client, "CONNECT", request.target, 403, "HTTP/2")
|
|
994
|
+
self._cancel_stream(stream_id)
|
|
995
|
+
self.streams.close(stream_id)
|
|
996
|
+
self._maybe_finish_after_goaway()
|
|
997
|
+
return
|
|
998
|
+
if self.config.http.connect_policy == 'allowlist' and not is_connect_allowed(host, port, self.config.http.connect_allow):
|
|
999
|
+
await self._send_response(stream_id, 403, [(b"content-type", b"text/plain")], b"connect denied")
|
|
1000
|
+
self.access_logger.log_http(self.client, "CONNECT", request.target, 403, "HTTP/2")
|
|
1001
|
+
self._cancel_stream(stream_id)
|
|
1002
|
+
self.streams.close(stream_id)
|
|
1003
|
+
self._maybe_finish_after_goaway()
|
|
1004
|
+
return
|
|
1005
|
+
try:
|
|
1006
|
+
upstream_reader, upstream_writer = await asyncio.wait_for(
|
|
1007
|
+
asyncio.open_connection(host, port),
|
|
1008
|
+
timeout=getattr(self.config, "read_timeout", 5.0),
|
|
1009
|
+
)
|
|
1010
|
+
except Exception:
|
|
1011
|
+
await self._send_response(stream_id, 502, [(b"content-type", b"text/plain")], b"bad gateway")
|
|
1012
|
+
self.access_logger.log_http(self.client, "CONNECT", request.target, 502, "HTTP/2")
|
|
1013
|
+
self._release_stream_work_lease(stream_id)
|
|
1014
|
+
self._cancel_stream(stream_id)
|
|
1015
|
+
self.streams.close(stream_id)
|
|
1016
|
+
self._maybe_finish_after_goaway()
|
|
1017
|
+
return
|
|
1018
|
+
tunnel = _HTTP2ConnectTunnel(
|
|
1019
|
+
handler=self,
|
|
1020
|
+
stream_id=stream_id,
|
|
1021
|
+
authority=request.target,
|
|
1022
|
+
upstream_reader=upstream_reader,
|
|
1023
|
+
upstream_writer=upstream_writer,
|
|
1024
|
+
work_lease=self.stream_work_leases.get(stream_id),
|
|
1025
|
+
)
|
|
1026
|
+
state.connect_tunnel = tunnel
|
|
1027
|
+
self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
|
|
1028
|
+
try:
|
|
1029
|
+
await tunnel.start()
|
|
1030
|
+
except Exception:
|
|
1031
|
+
state.connect_tunnel = None
|
|
1032
|
+
await close_tcp_writer(upstream_writer)
|
|
1033
|
+
raise
|
|
1034
|
+
if state.end_stream_received:
|
|
1035
|
+
await tunnel.feed_client_data(b'', end_stream=True)
|
|
1036
|
+
self.access_logger.log_http(self.client, "CONNECT", request.target, 200, "HTTP/2")
|
|
1037
|
+
|
|
1038
|
+
async def _send_h2_websocket_headers(
|
|
1039
|
+
self,
|
|
1040
|
+
stream_id: int,
|
|
1041
|
+
status: int,
|
|
1042
|
+
headers: list[tuple[bytes, bytes]],
|
|
1043
|
+
end_stream: bool,
|
|
1044
|
+
) -> None:
|
|
1045
|
+
await self._send_stream_headers(stream_id, status, headers, end_stream)
|
|
1046
|
+
|
|
1047
|
+
async def _start_websocket_stream(self, stream_id: int) -> None:
|
|
1048
|
+
state = self.streams.find(stream_id)
|
|
1049
|
+
if state is None:
|
|
1050
|
+
raise ProtocolError("websocket stream disappeared before dispatch")
|
|
1051
|
+
request = self._build_request(state)
|
|
1052
|
+
authority = self._pseudo_headers(state.headers).get(b":authority")
|
|
1053
|
+
if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
|
|
1054
|
+
await self._send_response(stream_id, 421, [(b"content-type", b"text/plain")], b"misdirected request")
|
|
1055
|
+
self.access_logger.log_http(self.client, "CONNECT", request.path, 421, "HTTP/2")
|
|
1056
|
+
self._release_stream_work_lease(stream_id)
|
|
1057
|
+
self._cancel_stream(stream_id)
|
|
1058
|
+
self.streams.close(stream_id)
|
|
1059
|
+
self._maybe_finish_after_goaway()
|
|
1060
|
+
return
|
|
1061
|
+
session = H2WebSocketSession(
|
|
1062
|
+
app=self.app,
|
|
1063
|
+
config=self.config,
|
|
1064
|
+
request=request,
|
|
1065
|
+
client=self.client,
|
|
1066
|
+
server=self.server,
|
|
1067
|
+
scheme=self.scheme,
|
|
1068
|
+
send_headers=lambda status, headers, end_stream: self._send_stream_headers(stream_id, status, headers, end_stream),
|
|
1069
|
+
send_data=lambda data, end_stream: self._send_stream_data(stream_id, data, end_stream=end_stream),
|
|
1070
|
+
metrics=self.metrics,
|
|
1071
|
+
on_close=lambda stream_id=stream_id: self._on_websocket_stream_closed(stream_id),
|
|
1072
|
+
)
|
|
1073
|
+
state.websocket_session = session
|
|
1074
|
+
self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
|
|
1075
|
+
await session.start()
|
|
1076
|
+
|
|
1077
|
+
def _validate_request_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
|
|
1078
|
+
pseudo_seen: set[bytes] = set()
|
|
1079
|
+
regular_seen = False
|
|
1080
|
+
allowed_pseudo = {b":method", b":scheme", b":authority", b":path", b":protocol"}
|
|
1081
|
+
for name, value in headers:
|
|
1082
|
+
if any(65 <= byte <= 90 for byte in name):
|
|
1083
|
+
raise ProtocolError("uppercase header field name forbidden in HTTP/2")
|
|
1084
|
+
if name.startswith(b":"):
|
|
1085
|
+
if regular_seen:
|
|
1086
|
+
raise ProtocolError("pseudo-header after regular header")
|
|
1087
|
+
if name not in allowed_pseudo:
|
|
1088
|
+
raise ProtocolError("invalid request pseudo-header")
|
|
1089
|
+
if name in pseudo_seen:
|
|
1090
|
+
raise ProtocolError("duplicate pseudo-header")
|
|
1091
|
+
pseudo_seen.add(name)
|
|
1092
|
+
else:
|
|
1093
|
+
regular_seen = True
|
|
1094
|
+
if name in {b"connection", b"upgrade", b"proxy-connection", b"transfer-encoding"}:
|
|
1095
|
+
raise ProtocolError("connection-specific header forbidden in HTTP/2")
|
|
1096
|
+
if name == b"te" and value.lower() != b"trailers":
|
|
1097
|
+
raise ProtocolError("invalid TE header for HTTP/2")
|
|
1098
|
+
if b":method" not in pseudo_seen:
|
|
1099
|
+
raise ProtocolError("missing :method pseudo-header")
|
|
1100
|
+
method = dict(headers).get(b":method", b"GET")
|
|
1101
|
+
protocol = dict(headers).get(b":protocol")
|
|
1102
|
+
if protocol is not None:
|
|
1103
|
+
if method != b"CONNECT":
|
|
1104
|
+
raise ProtocolError("extended CONNECT requires CONNECT method")
|
|
1105
|
+
if self.state.local_settings.get(SETTING_ENABLE_CONNECT_PROTOCOL, 0) != 1:
|
|
1106
|
+
raise ProtocolError("extended CONNECT not enabled")
|
|
1107
|
+
if b":scheme" not in pseudo_seen or b":path" not in pseudo_seen or b":authority" not in pseudo_seen:
|
|
1108
|
+
raise ProtocolError("extended CONNECT missing required pseudo-headers")
|
|
1109
|
+
return
|
|
1110
|
+
if method == b"CONNECT":
|
|
1111
|
+
if b":authority" not in pseudo_seen:
|
|
1112
|
+
raise ProtocolError("CONNECT missing :authority pseudo-header")
|
|
1113
|
+
if b":scheme" in pseudo_seen or b":path" in pseudo_seen:
|
|
1114
|
+
raise ProtocolError("CONNECT must not include :scheme or :path pseudo-headers")
|
|
1115
|
+
return
|
|
1116
|
+
if b":scheme" not in pseudo_seen or b":path" not in pseudo_seen:
|
|
1117
|
+
raise ProtocolError("missing required request pseudo-header")
|
|
1118
|
+
|
|
1119
|
+
def _build_request(self, state: H2StreamState) -> ParsedRequest:
|
|
1120
|
+
self._validate_request_headers(state.headers)
|
|
1121
|
+
pseudo = {k: v for k, v in state.headers if k.startswith(b":")}
|
|
1122
|
+
headers = [(k, v) for k, v in state.headers if not k.startswith(b":")]
|
|
1123
|
+
method = pseudo.get(b":method", b"GET").decode("ascii", "strict")
|
|
1124
|
+
if method.upper() == "CONNECT" and pseudo.get(b":protocol") != b"websocket":
|
|
1125
|
+
target = pseudo.get(b":authority", b"").decode("ascii", "strict")
|
|
1126
|
+
path = target
|
|
1127
|
+
raw_path = target.encode("ascii", "strict")
|
|
1128
|
+
query_string = b""
|
|
1129
|
+
else:
|
|
1130
|
+
target = pseudo.get(b":path", b"/").decode("ascii", "strict")
|
|
1131
|
+
split = urlsplit(target)
|
|
1132
|
+
path = split.path or "/"
|
|
1133
|
+
raw_path = path.encode("utf-8")
|
|
1134
|
+
query_string = split.query.encode("ascii")
|
|
1135
|
+
return ParsedRequest(
|
|
1136
|
+
method=method,
|
|
1137
|
+
target=target,
|
|
1138
|
+
path=path,
|
|
1139
|
+
raw_path=raw_path,
|
|
1140
|
+
query_string=query_string,
|
|
1141
|
+
http_version="2",
|
|
1142
|
+
headers=headers,
|
|
1143
|
+
body=state.body,
|
|
1144
|
+
keep_alive=True,
|
|
1145
|
+
expect_continue=False,
|
|
1146
|
+
websocket_upgrade=False,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
async def _run_stream(self, stream_id: int) -> None:
|
|
1150
|
+
state = self.streams.find(stream_id)
|
|
1151
|
+
if state is None:
|
|
1152
|
+
self._release_stream_work_lease(stream_id)
|
|
1153
|
+
return
|
|
1154
|
+
request = self._build_request(state)
|
|
1155
|
+
authority = self._pseudo_headers(state.headers).get(b":authority")
|
|
1156
|
+
try:
|
|
1157
|
+
if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
|
|
1158
|
+
await self._send_response(stream_id, 421, [(b"content-type", b"text/plain")], b"misdirected request")
|
|
1159
|
+
self.access_logger.log_http(self.client, request.method, request.path, 421, "HTTP/2")
|
|
1160
|
+
if self.streams.find(stream_id) is not None:
|
|
1161
|
+
self._cancel_stream(stream_id)
|
|
1162
|
+
self.streams.close(stream_id)
|
|
1163
|
+
self._maybe_finish_after_goaway()
|
|
1164
|
+
return
|
|
1165
|
+
status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(stream_id, request, allow_push=True)
|
|
1166
|
+
for interim_status, interim_headers in informational:
|
|
1167
|
+
await self._send_stream_headers(stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
|
|
1168
|
+
try:
|
|
1169
|
+
await self._send_response(stream_id, status, headers, body, trailers, body_segments=body_segments)
|
|
1170
|
+
finally:
|
|
1171
|
+
if cleanup is not None:
|
|
1172
|
+
cleanup()
|
|
1173
|
+
self.access_logger.log_http(self.client, request.method, request.path, status, "HTTP/2")
|
|
1174
|
+
if self.streams.find(stream_id) is not None:
|
|
1175
|
+
self._cancel_stream(stream_id)
|
|
1176
|
+
self.streams.close(stream_id)
|
|
1177
|
+
self._maybe_finish_after_goaway()
|
|
1178
|
+
finally:
|
|
1179
|
+
self._release_stream_work_lease(stream_id)
|
|
1180
|
+
|
|
1181
|
+
async def _send_response(self, stream_id: int, status: int, headers: list[tuple[bytes, bytes]], body: bytes, trailers: list[tuple[bytes, bytes]] | None = None, *, body_segments: list | None = None) -> None:
|
|
1182
|
+
state = self.streams.find(stream_id)
|
|
1183
|
+
if state is None or state.closed:
|
|
1184
|
+
raise ProtocolError("attempted to send response on a closed HTTP/2 stream")
|
|
1185
|
+
streamed_body = response_body_segments_have_bytes(body_segments or []) if body_segments is not None else False
|
|
1186
|
+
if state.reserved_local and not state.opened:
|
|
1187
|
+
state.open_local_reserved(end_stream=not body and not streamed_body and not bool(trailers))
|
|
1188
|
+
headers = apply_response_header_policy(
|
|
1189
|
+
strip_connection_specific_headers(headers),
|
|
1190
|
+
server_header=self.config.server_header_value,
|
|
1191
|
+
include_date_header=self.config.include_date_header,
|
|
1192
|
+
default_headers=self.config.default_response_headers,
|
|
1193
|
+
alt_svc_values=configured_alt_svc_values(self.config, request_http_version='2'),
|
|
1194
|
+
)
|
|
1195
|
+
header_block = self.hpack_encoder.encode_header_block([(b":status", str(status).encode("ascii")), *headers])
|
|
1196
|
+
trailers = list(trailers or [])
|
|
1197
|
+
end_after_headers = not body and not streamed_body and not trailers
|
|
1198
|
+
await self._write_raw(self.frame_writer.headers(stream_id, header_block, end_stream=end_after_headers))
|
|
1199
|
+
if body_segments is not None:
|
|
1200
|
+
if not streamed_body and not trailers:
|
|
1201
|
+
state.send_end_stream()
|
|
1202
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1203
|
+
return
|
|
1204
|
+
if streamed_body:
|
|
1205
|
+
async for chunk in iter_response_body_segments(body_segments, chunk_size=self.state.max_frame_size):
|
|
1206
|
+
await self._send_stream_data(stream_id, chunk, end_stream=False)
|
|
1207
|
+
if trailers:
|
|
1208
|
+
trailer_block = self.hpack_encoder.encode_header_block(trailers)
|
|
1209
|
+
await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
|
|
1210
|
+
state.send_end_stream()
|
|
1211
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1212
|
+
return
|
|
1213
|
+
await self._send_stream_data(stream_id, b'', end_stream=True)
|
|
1214
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1215
|
+
return
|
|
1216
|
+
if not body and not trailers:
|
|
1217
|
+
state.send_end_stream()
|
|
1218
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1219
|
+
return
|
|
1220
|
+
if not body and trailers:
|
|
1221
|
+
trailer_block = self.hpack_encoder.encode_header_block(trailers)
|
|
1222
|
+
await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
|
|
1223
|
+
state.send_end_stream()
|
|
1224
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1225
|
+
return
|
|
1226
|
+
offset = 0
|
|
1227
|
+
while offset < len(body):
|
|
1228
|
+
chunk_size = min(self.state.max_frame_size, len(body) - offset)
|
|
1229
|
+
while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
|
|
1230
|
+
await self._wait_for_credit(stream_id)
|
|
1231
|
+
allowed = min(chunk_size, self.state.connection_send_window.available, state.send_window.available)
|
|
1232
|
+
if allowed <= 0:
|
|
1233
|
+
await self._wait_for_credit(stream_id)
|
|
1234
|
+
continue
|
|
1235
|
+
chunk = body[offset : offset + allowed]
|
|
1236
|
+
offset += len(chunk)
|
|
1237
|
+
self.state.connection_send_window.consume(len(chunk))
|
|
1238
|
+
state.send_window.consume(len(chunk))
|
|
1239
|
+
final_chunk = offset == len(body)
|
|
1240
|
+
end_stream = final_chunk and not trailers
|
|
1241
|
+
await self._write_raw(self.frame_writer.data(stream_id, chunk, end_stream=end_stream))
|
|
1242
|
+
if final_chunk and trailers:
|
|
1243
|
+
trailer_block = self.hpack_encoder.encode_header_block(trailers)
|
|
1244
|
+
await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
|
|
1245
|
+
state.send_end_stream()
|
|
1246
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1247
|
+
elif final_chunk:
|
|
1248
|
+
state.send_end_stream()
|
|
1249
|
+
self._finalize_stream_if_complete(stream_id)
|
|
1250
|
+
|
|
1251
|
+
async def _wait_for_credit(self, stream_id: int) -> None:
|
|
1252
|
+
state = self.streams.find(stream_id)
|
|
1253
|
+
if state is None or state.closed:
|
|
1254
|
+
raise ProtocolError("attempted to wait for flow-control credit on a closed stream")
|
|
1255
|
+
waiter = self.waiters.setdefault(stream_id, FlowWaiter(state.send_window))
|
|
1256
|
+
waiter.notify()
|
|
1257
|
+
while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
|
|
1258
|
+
await waiter.wait()
|
|
1259
|
+
state = self.streams.find(stream_id)
|
|
1260
|
+
if state is None or state.closed:
|
|
1261
|
+
raise ProtocolError("stream closed while waiting for flow-control credit")
|
|
1262
|
+
|
|
1263
|
+
async def _write_raw(self, data: bytes, *, record_activity: bool = True) -> None:
|
|
1264
|
+
async with self.writer_lock:
|
|
1265
|
+
self.writer.write(data)
|
|
1266
|
+
await self.writer.drain()
|
|
1267
|
+
if record_activity:
|
|
1268
|
+
self._record_keepalive_activity()
|
|
1269
|
+
|
|
1270
|
+
def _notify_waiter(self, stream_id: int) -> None:
|
|
1271
|
+
if stream_id == 0:
|
|
1272
|
+
for waiter in self.waiters.values():
|
|
1273
|
+
waiter.notify()
|
|
1274
|
+
return
|
|
1275
|
+
waiter = self.waiters.get(stream_id)
|
|
1276
|
+
if waiter is not None:
|
|
1277
|
+
waiter.notify()
|
|
1278
|
+
|
|
1279
|
+
def _cancel_stream(self, stream_id: int) -> None:
|
|
1280
|
+
self._release_stream_work_lease(stream_id)
|
|
1281
|
+
task = self.stream_tasks.pop(stream_id, None)
|
|
1282
|
+
if task is not None:
|
|
1283
|
+
task.cancel()
|
|
1284
|
+
self.waiters.pop(stream_id, None)
|
|
1285
|
+
|
|
1286
|
+
async def _shutdown_streams(self) -> None:
|
|
1287
|
+
for state in list(self.streams.streams.values()):
|
|
1288
|
+
if state.websocket_session is not None:
|
|
1289
|
+
with suppress(Exception):
|
|
1290
|
+
await state.websocket_session.abort()
|
|
1291
|
+
if state.connect_tunnel is not None:
|
|
1292
|
+
with suppress(Exception):
|
|
1293
|
+
await state.connect_tunnel.abort()
|
|
1294
|
+
for stream_id, task in list(self.stream_tasks.items()):
|
|
1295
|
+
task.cancel()
|
|
1296
|
+
with suppress(asyncio.CancelledError):
|
|
1297
|
+
await task
|
|
1298
|
+
self.stream_tasks.pop(stream_id, None)
|
|
1299
|
+
if not self.state.local_goaway_sent:
|
|
1300
|
+
self.state.local_goaway_sent = True
|
|
1301
|
+
self.state.local_goaway_last_stream_id = self.state.last_stream_id
|
|
1302
|
+
with suppress(Exception):
|
|
1303
|
+
await self._write_raw(serialize_goaway(self.state.last_stream_id))
|