tigrcorn-runtime 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.
@@ -0,0 +1,878 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import random
5
+ from contextlib import suppress
6
+ from typing import Any
7
+
8
+ from tigrcorn_asgi.receive import HTTPRequestReceive, HTTPStreamingRequestReceive
9
+ from tigrcorn_asgi.scopes.http import build_http_scope
10
+ from tigrcorn_asgi.send import FileBodySegment, HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
11
+ from tigrcorn_runtime.app_interfaces import resolve_app_dispatch
12
+ from tigrcorn_core.errors import ProtocolError
13
+ from tigrcorn_config.model import ListenerConfig, ServerConfig
14
+ from tigrcorn_core.constants import H2_PREFACE
15
+ from tigrcorn_transports.listeners.inproc import InProcListener
16
+ from tigrcorn_transports.listeners.pipe import PipeListener
17
+ from tigrcorn_transports.listeners.tcp import TCPListener
18
+ from tigrcorn_transports.listeners.udp import UDPListener
19
+ from tigrcorn_transports.listeners.unix import UnixListener
20
+ from tigrcorn_observability.logging import AccessLogger, configure_logging, resolve_logging_config
21
+ from tigrcorn_observability.metrics import StatsdExporter
22
+ from tigrcorn_observability.tracing import OtelExporter, span
23
+ from tigrcorn_protocols.connect import is_connect_allowed, parse_connect_authority
24
+ from tigrcorn_http.alt_svc import configured_alt_svc_values
25
+ from tigrcorn_http.entity import apply_response_entity_semantics, plan_file_backed_response_entity_semantics
26
+ from tigrcorn_protocols.http1.keepalive import apply_keep_alive_policy
27
+ from tigrcorn_protocols.http1.parser import ParsedRequestHead, read_http11_request_head
28
+ from tigrcorn_protocols.http1.serializer import finalize_chunked_body, serialize_http11_response_chunk, serialize_http11_response_head, serialize_http11_response_whole
29
+ from tigrcorn_protocols.http2.handler import HTTP2ConnectionHandler
30
+ from tigrcorn_protocols.http3.handler import HTTP3DatagramHandler
31
+ from tigrcorn_protocols.lifespan.driver import LifespanManager
32
+ from tigrcorn_protocols.rawframed.handler import RawFramedApplicationHandler
33
+ from tigrcorn_protocols.websocket.handler import WebSocketConnectionHandler
34
+ from tigrcorn_protocols.scheduler import ProductionScheduler, SchedulerPolicy
35
+ from tigrcorn_security.tls import build_server_ssl_context, tls_extension_payload
36
+ from tigrcorn_runtime.server.hooks import run_async_hooks
37
+ from tigrcorn_runtime.server.state import ServerState
38
+ from tigrcorn_transports.tcp.reader import PrebufferedReader
39
+ from tigrcorn_core.types import ASGIApp, StreamReaderLike
40
+ from tigrcorn_core.utils.authority import authority_allowed
41
+ from tigrcorn_core.utils.headers import get_header
42
+ from tigrcorn_core.utils.net import peer_parts
43
+ from tigrcorn_core.utils.proxy import resolve_proxy_view
44
+
45
+
46
+ class TigrCornServer:
47
+ def __init__(self, app: ASGIApp, config: ServerConfig) -> None:
48
+ selection = resolve_app_dispatch(app, config.app.interface)
49
+ self.app = selection.app
50
+ self.app_interface = selection.interface
51
+ self.config = config
52
+ self._resolved_logging = resolve_logging_config(config.log_level, config=config.logging)
53
+ self.logger = configure_logging(config.log_level, config=config.logging)
54
+ self.access_logger = AccessLogger(
55
+ self.logger,
56
+ enabled=self._resolved_logging.access_log,
57
+ fmt=self._resolved_logging.access_log_format,
58
+ )
59
+ self.state = ServerState()
60
+ self.lifespan = LifespanManager(app, mode=config.lifespan)
61
+ self._listeners: list[TCPListener | UDPListener | UnixListener | PipeListener | InProcListener] = []
62
+ self._datagram_handlers: list[HTTP3DatagramHandler] = []
63
+ self._should_exit = asyncio.Event()
64
+ self._started = False
65
+ self._metrics_server: asyncio.AbstractServer | None = None
66
+ self._request_budget_task: asyncio.Task[None] | None = None
67
+ self._statsd_exporter = StatsdExporter(config.metrics.statsd_host, logger=self.logger) if config.metrics.statsd_host else None
68
+ self._otel_exporter = OtelExporter(config.metrics.otel_endpoint, logger=self.logger) if config.metrics.otel_endpoint else None
69
+ policy = SchedulerPolicy()
70
+ if config.scheduler.max_connections is not None:
71
+ policy.max_connections = config.scheduler.max_connections
72
+ if config.scheduler.max_tasks is not None:
73
+ policy.max_tasks = config.scheduler.max_tasks
74
+ if config.scheduler.max_streams is not None:
75
+ policy.max_streams_per_session = config.scheduler.max_streams
76
+ if config.scheduler.limit_concurrency is not None:
77
+ policy.limit_concurrency = config.scheduler.limit_concurrency
78
+ self.scheduler = ProductionScheduler(policy)
79
+ self._request_budget = None
80
+ if config.process.limit_max_requests is not None:
81
+ jitter = max(0, config.process.max_requests_jitter)
82
+ self._request_budget = config.process.limit_max_requests + (random.randint(0, jitter) if jitter else 0)
83
+
84
+ async def start(self) -> None:
85
+ if self._started:
86
+ return
87
+ with span('server.start', attrs={'listener_count': len(self.config.listeners)}, sink=self._otel_exporter.record_span if self._otel_exporter is not None else None):
88
+ await self.lifespan.startup()
89
+ await run_async_hooks(self.config.hooks.on_startup, self)
90
+ for listener_cfg in self.config.listeners:
91
+ listener = await self._make_listener(listener_cfg)
92
+ await listener.start(self._make_client_handler(listener_cfg))
93
+ self._sync_listener_bound_address(listener_cfg, listener)
94
+ self._listeners.append(listener)
95
+ self.logger.info('listening on %s', listener_cfg.label)
96
+ if self.config.metrics.enabled and self.config.metrics.bind:
97
+ self._metrics_server = await self._start_metrics_endpoint(self.config.metrics.bind)
98
+ if self._statsd_exporter is not None:
99
+ await self._statsd_exporter.start(self.state.metrics)
100
+ if self._otel_exporter is not None:
101
+ await self._otel_exporter.start(self.state.metrics)
102
+ if self._request_budget is not None:
103
+ self._request_budget_task = asyncio.create_task(self._monitor_request_budget(), name='tigrcorn-request-budget')
104
+ self._started = True
105
+
106
+ async def serve_forever(self) -> None:
107
+ await self.start()
108
+ try:
109
+ await self._should_exit.wait()
110
+ finally:
111
+ await self.close()
112
+
113
+ @staticmethod
114
+ def _sync_listener_bound_address(cfg: ListenerConfig, listener: Any) -> None:
115
+ server = getattr(listener, 'server', None)
116
+ sockets = getattr(server, 'sockets', None) if server is not None else None
117
+ if sockets:
118
+ sockname = sockets[0].getsockname()
119
+ if isinstance(sockname, tuple) and len(sockname) >= 2:
120
+ cfg.host = str(sockname[0])
121
+ cfg.port = int(sockname[1])
122
+ return
123
+ if isinstance(sockname, str):
124
+ cfg.path = sockname
125
+ return
126
+ transport = getattr(listener, 'transport', None)
127
+ if transport is not None:
128
+ sockname = transport.get_extra_info('sockname')
129
+ if isinstance(sockname, tuple) and len(sockname) >= 2:
130
+ cfg.host = str(sockname[0])
131
+ cfg.port = int(sockname[1])
132
+ return
133
+ if isinstance(sockname, str):
134
+ cfg.path = sockname
135
+ return
136
+
137
+ def request_shutdown(self) -> None:
138
+ self._should_exit.set()
139
+
140
+ async def close(self) -> None:
141
+ if self.state.shutting_down:
142
+ return
143
+ self.state.shutting_down = True
144
+ with span('server.shutdown', attrs={'active_listeners': len(self._listeners)}, sink=self._otel_exporter.record_span if self._otel_exporter is not None else None):
145
+ if self._request_budget_task is not None:
146
+ self._request_budget_task.cancel()
147
+ with suppress(Exception):
148
+ await self._request_budget_task
149
+ if self._metrics_server is not None:
150
+ self._metrics_server.close()
151
+ with suppress(Exception):
152
+ await self._metrics_server.wait_closed()
153
+ self._metrics_server = None
154
+ for listener in self._listeners:
155
+ with suppress(Exception):
156
+ await listener.close()
157
+ self._listeners.clear()
158
+ for handler in self._datagram_handlers:
159
+ with suppress(Exception):
160
+ await handler.close()
161
+ self._datagram_handlers.clear()
162
+ with suppress(Exception):
163
+ await asyncio.wait_for(self.scheduler.close(), timeout=self.config.http.shutdown_timeout)
164
+ with suppress(Exception):
165
+ await self.lifespan.shutdown()
166
+ with suppress(Exception):
167
+ await run_async_hooks(self.config.hooks.on_shutdown, self)
168
+ if self._statsd_exporter is not None:
169
+ with suppress(Exception):
170
+ await self._statsd_exporter.stop(self.state.metrics)
171
+ if self._otel_exporter is not None:
172
+ with suppress(Exception):
173
+ await self._otel_exporter.stop(self.state.metrics)
174
+
175
+ async def _make_listener(self, cfg: ListenerConfig):
176
+ if cfg.kind == 'tcp':
177
+ ssl_ctx = build_server_ssl_context(cfg)
178
+ return TCPListener(
179
+ cfg.host,
180
+ cfg.port,
181
+ cfg.backlog,
182
+ ssl=ssl_ctx,
183
+ reuse_port=cfg.reuse_port,
184
+ reuse_address=cfg.reuse_address,
185
+ nodelay=cfg.nodelay,
186
+ fd=cfg.fd,
187
+ )
188
+ if cfg.kind == 'udp':
189
+ return UDPListener(cfg.host, cfg.port, reuse_port=cfg.reuse_port, fd=cfg.fd)
190
+ if cfg.kind == 'unix':
191
+ ssl_ctx = build_server_ssl_context(cfg)
192
+ return UnixListener(cfg.path or '', cfg.backlog, ssl=ssl_ctx, fd=cfg.fd)
193
+ if cfg.kind == 'pipe':
194
+ return PipeListener(cfg.path or '')
195
+ return InProcListener()
196
+
197
+ def _make_client_handler(self, listener_cfg: ListenerConfig):
198
+ if listener_cfg.kind == 'udp':
199
+ h3_handler = HTTP3DatagramHandler(
200
+ app=self.app,
201
+ config=self.config,
202
+ listener=listener_cfg,
203
+ access_logger=self.access_logger,
204
+ scheduler=self.scheduler,
205
+ metrics=self.state.metrics,
206
+ )
207
+ self._datagram_handlers.append(h3_handler)
208
+
209
+ async def udp_handler(packet, endpoint) -> None:
210
+ sessions_before = len(h3_handler.sessions)
211
+ responses_before = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
212
+ await h3_handler.handle_packet(packet, endpoint)
213
+ if len(h3_handler.sessions) > sessions_before:
214
+ self.state.metrics.connection_opened()
215
+ responses_after = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
216
+ if responses_after > responses_before:
217
+ self.state.metrics.requests_served += responses_after - responses_before
218
+
219
+ return udp_handler
220
+
221
+ if listener_cfg.kind == 'pipe':
222
+ raw_handler = RawFramedApplicationHandler(
223
+ app=self.app,
224
+ config=self.config,
225
+ listener=listener_cfg,
226
+ access_logger=self.access_logger,
227
+ )
228
+
229
+ async def pipe_handler(connection, data) -> None:
230
+ handled = await raw_handler.feed_bytes(connection, data, path=listener_cfg.path)
231
+ self.state.metrics.requests_served += handled
232
+ self.state.metrics.bytes_received += len(data)
233
+
234
+ return pipe_handler
235
+
236
+ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
237
+ await self._handle_client(reader, writer, listener_cfg)
238
+
239
+ return handler
240
+
241
+ async def _handle_client(
242
+ self,
243
+ reader: asyncio.StreamReader,
244
+ writer: asyncio.StreamWriter,
245
+ listener_cfg: ListenerConfig,
246
+ ) -> None:
247
+ lease = self.scheduler.acquire_connection()
248
+ if lease is None:
249
+ writer.close()
250
+ with suppress(Exception):
251
+ await writer.wait_closed()
252
+ return
253
+ self.state.metrics.connection_opened()
254
+ peername = writer.get_extra_info('peername')
255
+ sockname = writer.get_extra_info('sockname')
256
+ ssl_obj = writer.get_extra_info('ssl_object')
257
+ selected_alpn = ssl_obj.selected_alpn_protocol() if ssl_obj else None
258
+ tls_payload = tls_extension_payload(writer)
259
+ scope_tls_extensions = {'tls': tls_payload} if tls_payload is not None else None
260
+ client_host, client_port = peer_parts(peername)
261
+ server_host, server_port = peer_parts(sockname)
262
+ client = (client_host, client_port) if client_host is not None and client_port is not None else None
263
+ server = (server_host or '', server_port)
264
+ scheme = 'https' if ssl_obj else (listener_cfg.scheme or 'http')
265
+ ws_scheme = 'wss' if ssl_obj else 'ws'
266
+ try:
267
+ if selected_alpn == 'h2' and '2' in listener_cfg.http_versions:
268
+ h2_handler = HTTP2ConnectionHandler(
269
+ app=self.app,
270
+ config=self.config,
271
+ access_logger=self.access_logger,
272
+ scheduler=self.scheduler,
273
+ metrics=self.state.metrics,
274
+ reader=reader,
275
+ writer=writer,
276
+ client=client,
277
+ server=server,
278
+ scheme=scheme,
279
+ scope_extensions=scope_tls_extensions,
280
+ )
281
+ await h2_handler.handle()
282
+ return
283
+
284
+ initial = b''
285
+ if '2' in listener_cfg.http_versions and self.config.enable_h2c:
286
+ initial = await self._read_preface_probe(reader)
287
+ if initial == H2_PREFACE:
288
+ h2_handler = HTTP2ConnectionHandler(
289
+ app=self.app,
290
+ config=self.config,
291
+ access_logger=self.access_logger,
292
+ scheduler=self.scheduler,
293
+ metrics=self.state.metrics,
294
+ reader=reader,
295
+ writer=writer,
296
+ client=client,
297
+ server=server,
298
+ scheme=scheme,
299
+ prebuffer=initial,
300
+ scope_extensions=scope_tls_extensions,
301
+ )
302
+ await h2_handler.handle()
303
+ return
304
+
305
+ buffered_reader: StreamReaderLike = PrebufferedReader(reader, initial)
306
+ await self._handle_http11_connection(
307
+ buffered_reader,
308
+ writer,
309
+ listener_cfg,
310
+ client=client,
311
+ server=server,
312
+ scheme=scheme,
313
+ ws_scheme=ws_scheme,
314
+ scope_extensions=scope_tls_extensions,
315
+ )
316
+ finally:
317
+ lease.release()
318
+ self.state.metrics.connection_closed()
319
+ writer.close()
320
+ with suppress(Exception):
321
+ await writer.wait_closed()
322
+
323
+ async def _read_preface_probe(self, reader: asyncio.StreamReader) -> bytes:
324
+ data = await asyncio.wait_for(reader.read(len(H2_PREFACE)), timeout=self.config.http.read_timeout)
325
+ if not data:
326
+ return b''
327
+ if H2_PREFACE.startswith(data) and data != H2_PREFACE:
328
+ with suppress(Exception):
329
+ data += await asyncio.wait_for(reader.readexactly(len(H2_PREFACE) - len(data)), timeout=0.05)
330
+ return data
331
+
332
+ async def _handle_http11_connection(
333
+ self,
334
+ reader: StreamReaderLike,
335
+ writer: asyncio.StreamWriter,
336
+ listener_cfg: ListenerConfig,
337
+ *,
338
+ client: tuple[str, int] | None,
339
+ server: tuple[str, int] | tuple[str, None] | None,
340
+ scheme: str,
341
+ ws_scheme: str,
342
+ scope_extensions: dict | None = None,
343
+ ) -> None:
344
+ keep_handling = True
345
+ handled_requests = 0
346
+ while keep_handling and not self.state.shutting_down:
347
+ request_timeout = self.config.http.keep_alive_timeout if handled_requests else self.config.http.read_timeout
348
+ if self.config.http.http1_header_read_timeout is not None:
349
+ request_timeout = min(request_timeout, self.config.http.http1_header_read_timeout)
350
+ try:
351
+ request = await asyncio.wait_for(
352
+ read_http11_request_head(
353
+ reader,
354
+ max_body_size=self.config.max_body_size,
355
+ max_header_size=self.config.max_header_size,
356
+ max_incomplete_event_size=self.config.http.http1_max_incomplete_event_size,
357
+ buffer_size=self.config.http.http1_buffer_size,
358
+ ),
359
+ timeout=request_timeout,
360
+ )
361
+ except asyncio.TimeoutError:
362
+ break
363
+ except Exception as exc:
364
+ self.state.metrics.protocol_errors += 1
365
+ self.logger.warning('protocol error from %s: %s', client, exc)
366
+ await self._write_error(writer, 400, b'bad request', keep_alive=False)
367
+ break
368
+ if request is None:
369
+ break
370
+
371
+ proxy_view = resolve_proxy_view(
372
+ request.headers,
373
+ client=client,
374
+ server=server,
375
+ scheme=scheme,
376
+ root_path=self.config.proxy.root_path,
377
+ enabled=self.config.proxy.proxy_headers,
378
+ forwarded_allow_ips=self.config.proxy.forwarded_allow_ips,
379
+ )
380
+ request_client = proxy_view.client
381
+ request_server = proxy_view.server
382
+ request_scheme = proxy_view.scheme
383
+ request_ws_scheme = 'wss' if request_scheme == 'https' else 'ws'
384
+ request.keep_alive = apply_keep_alive_policy(request.keep_alive, enabled=self.config.http.http1_keep_alive)
385
+
386
+ if request.method.upper() == 'CONNECT':
387
+ await self._handle_http11_connect_tunnel(reader, writer, request, client=request_client)
388
+ keep_handling = False
389
+ break
390
+
391
+ if request.websocket_upgrade:
392
+ if not listener_cfg.websocket:
393
+ await self._write_error(writer, 426, b'websocket not enabled', keep_alive=False)
394
+ break
395
+ work_lease = self.scheduler.acquire_work()
396
+ if work_lease is None:
397
+ self.state.metrics.scheduler_task_rejected()
398
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
399
+ break
400
+ handler = WebSocketConnectionHandler(
401
+ app=self.app,
402
+ config=self.config,
403
+ access_logger=self.access_logger,
404
+ request=request,
405
+ reader=reader,
406
+ writer=writer,
407
+ client=request_client,
408
+ server=request_server,
409
+ scheme=request_ws_scheme,
410
+ scope_extensions=scope_extensions,
411
+ metrics=self.state.metrics,
412
+ )
413
+ try:
414
+ self.state.metrics.websocket_opened()
415
+ await handler.handle()
416
+ finally:
417
+ work_lease.release()
418
+ self.state.metrics.websocket_closed()
419
+ keep_handling = False
420
+ break
421
+
422
+ work_lease = self.scheduler.acquire_work()
423
+ if work_lease is None:
424
+ self.state.metrics.scheduler_task_rejected()
425
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
426
+ break
427
+ try:
428
+ keep_handling = await self._serve_http11_request(
429
+ reader,
430
+ writer,
431
+ request,
432
+ client=request_client,
433
+ server=request_server,
434
+ scheme=request_scheme,
435
+ scope_extensions=scope_extensions,
436
+ )
437
+ finally:
438
+ work_lease.release()
439
+ handled_requests += 1
440
+
441
+ async def _drain_writer(self, writer: asyncio.StreamWriter) -> None:
442
+ await asyncio.wait_for(writer.drain(), timeout=self.config.http.write_timeout)
443
+
444
+ async def _write_continue(self, writer: asyncio.StreamWriter) -> None:
445
+ writer.write(b'HTTP/1.1 100 Continue\r\n\r\n')
446
+ await self._drain_writer(writer)
447
+
448
+ def _build_http11_receive(
449
+ self,
450
+ reader: StreamReaderLike,
451
+ writer: asyncio.StreamWriter,
452
+ request: ParsedRequestHead,
453
+ ) -> HTTPRequestReceive | HTTPStreamingRequestReceive:
454
+ if request.body_kind == 'none':
455
+ return HTTPRequestReceive(b'')
456
+ return HTTPStreamingRequestReceive(
457
+ reader=reader,
458
+ content_length=request.content_length if request.body_kind == 'content-length' else None,
459
+ chunked=request.body_kind == 'chunked',
460
+ max_body_size=self.config.max_body_size,
461
+ expect_continue=request.expect_continue,
462
+ on_expect_continue=lambda: self._write_continue(writer),
463
+ max_chunk_size=self.config.http.http1_buffer_size,
464
+ trailer_policy=self.config.http.trailer_policy,
465
+ )
466
+
467
+ def _http11_scope_extensions(self, request: ParsedRequestHead, *, scope_extensions: dict | None = None) -> dict:
468
+ extensions: dict = dict(scope_extensions or {})
469
+ if request.body_kind == 'chunked' and self.config.http.trailer_policy != 'drop':
470
+ extensions['tigrcorn.http.request_trailers'] = {}
471
+ if request.method.upper() == 'CONNECT':
472
+ extensions['tigrcorn.http.connect'] = {'authority': request.target}
473
+ extensions['tigrcorn.http.response.file'] = {'protocol': 'http/1.1', 'streaming': True, 'sendfile': True}
474
+ extensions['http.response.pathsend'] = {}
475
+ return extensions
476
+
477
+ @staticmethod
478
+ def _parse_connect_authority(authority: str) -> tuple[str, int]:
479
+ return parse_connect_authority(authority)
480
+
481
+ async def _relay_stream(self, reader: StreamReaderLike, writer: asyncio.StreamWriter) -> None:
482
+ try:
483
+ while True:
484
+ chunk = await asyncio.wait_for(reader.read(65536), timeout=self.config.http.idle_timeout)
485
+ if not chunk:
486
+ break
487
+ writer.write(chunk)
488
+ await self._drain_writer(writer)
489
+ self.state.metrics.bytes_sent += len(chunk)
490
+ finally:
491
+ writer.close()
492
+ with suppress(Exception):
493
+ await writer.wait_closed()
494
+
495
+ async def _try_http11_sendfile(self, writer: asyncio.StreamWriter, segment: FileBodySegment) -> bool:
496
+ if segment.count is not None and segment.count <= 0:
497
+ return True
498
+ if writer.get_extra_info('ssl_object') is not None or writer.get_extra_info('sslcontext') is not None:
499
+ return False
500
+ transport = getattr(writer, 'transport', None) or getattr(writer, '_transport', None)
501
+ if transport is None:
502
+ return False
503
+ loop = asyncio.get_running_loop()
504
+ try:
505
+ with open(segment.path, 'rb') as handle:
506
+ await loop.sendfile(transport, handle, offset=segment.offset, count=segment.count, fallback=False)
507
+ return True
508
+ except Exception:
509
+ return False
510
+
511
+ async def _send_http11_body_segments(self, writer: asyncio.StreamWriter, body_segments: list, *, chunked: bool = False) -> None:
512
+ if not chunked and len(body_segments) == 1 and isinstance(body_segments[0], FileBodySegment):
513
+ if await self._try_http11_sendfile(writer, body_segments[0]):
514
+ return
515
+ async for chunk in iter_response_body_segments(body_segments):
516
+ self.state.metrics.bytes_sent += len(chunk)
517
+ if chunked:
518
+ writer.write(serialize_http11_response_chunk(chunk))
519
+ else:
520
+ writer.write(chunk)
521
+ if len(chunk) >= 64 * 1024:
522
+ await self._drain_writer(writer)
523
+ await self._drain_writer(writer)
524
+
525
+ async def _send_http11_streamed_response(
526
+ self,
527
+ writer: asyncio.StreamWriter,
528
+ *,
529
+ request: ParsedRequestHead,
530
+ status: int,
531
+ headers: list[tuple[bytes, bytes]],
532
+ body_segments: list,
533
+ trailers: list[tuple[bytes, bytes]],
534
+ ) -> None:
535
+ has_body = response_body_segments_have_bytes(body_segments)
536
+ if trailers:
537
+ writer.write(
538
+ serialize_http11_response_head(
539
+ status=status,
540
+ headers=headers,
541
+ keep_alive=request.keep_alive,
542
+ server_header=self.config.server_header_value,
543
+ chunked=True,
544
+ include_date_header=self.config.include_date_header,
545
+ default_headers=self.config.default_response_headers,
546
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
547
+ )
548
+ )
549
+ await self._drain_writer(writer)
550
+ if has_body:
551
+ await self._send_http11_body_segments(writer, body_segments, chunked=True)
552
+ writer.write(finalize_chunked_body(trailers))
553
+ await self._drain_writer(writer)
554
+ return
555
+ if not has_body:
556
+ writer.write(
557
+ serialize_http11_response_whole(
558
+ status=status,
559
+ headers=headers,
560
+ body=b'',
561
+ keep_alive=request.keep_alive,
562
+ server_header=self.config.server_header_value,
563
+ include_date_header=self.config.include_date_header,
564
+ default_headers=self.config.default_response_headers,
565
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
566
+ )
567
+ )
568
+ await self._drain_writer(writer)
569
+ return
570
+ writer.write(
571
+ serialize_http11_response_head(
572
+ status=status,
573
+ headers=headers,
574
+ keep_alive=request.keep_alive,
575
+ server_header=self.config.server_header_value,
576
+ chunked=False,
577
+ include_date_header=self.config.include_date_header,
578
+ default_headers=self.config.default_response_headers,
579
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
580
+ )
581
+ )
582
+ await self._drain_writer(writer)
583
+ await self._send_http11_body_segments(writer, body_segments, chunked=False)
584
+
585
+ async def _handle_http11_connect_tunnel(
586
+ self,
587
+ reader: StreamReaderLike,
588
+ writer: asyncio.StreamWriter,
589
+ request: ParsedRequestHead,
590
+ *,
591
+ client: tuple[str, int] | None,
592
+ ) -> None:
593
+ try:
594
+ host, port = self._parse_connect_authority(request.target)
595
+ except Exception:
596
+ await self._write_error(writer, 400, b'bad connect target', keep_alive=False)
597
+ return
598
+ if self.config.http.connect_policy == 'deny':
599
+ await self._write_error(writer, 403, b'connect denied', keep_alive=False)
600
+ return
601
+ if self.config.http.connect_policy == 'allowlist' and not is_connect_allowed(host, port, self.config.http.connect_allow):
602
+ await self._write_error(writer, 403, b'connect denied', keep_alive=False)
603
+ return
604
+ if request.body_kind != 'none':
605
+ await self._write_error(writer, 400, b'connect request body not supported', keep_alive=False)
606
+ return
607
+ work_lease = self.scheduler.acquire_work()
608
+ if work_lease is None:
609
+ self.state.metrics.scheduler_task_rejected()
610
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
611
+ return
612
+ try:
613
+ upstream_reader, upstream_writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=self.config.http.read_timeout)
614
+ except Exception:
615
+ work_lease.release()
616
+ await self._write_error(writer, 502, b'bad gateway', keep_alive=False)
617
+ return
618
+ writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n')
619
+ await self._drain_writer(writer)
620
+ self.access_logger.log_http(client, 'CONNECT', request.target, 200, f'HTTP/{request.http_version}')
621
+ try:
622
+ self.state.metrics.scheduler_task_spawned()
623
+ relay_up = self.scheduler.spawn(self._relay_stream(reader, upstream_writer), owner=f'connect:{request.target}:up')
624
+ self.state.metrics.scheduler_task_spawned()
625
+ relay_down = self.scheduler.spawn(self._relay_stream(upstream_reader, writer), owner=f'connect:{request.target}:down')
626
+ except RuntimeError:
627
+ self.state.metrics.scheduler_task_rejected()
628
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
629
+ return
630
+ try:
631
+ done, pending = await asyncio.wait({relay_up, relay_down}, return_when=asyncio.FIRST_COMPLETED)
632
+ for task in pending:
633
+ task.cancel()
634
+ with suppress(Exception):
635
+ await task
636
+ for task in done:
637
+ with suppress(Exception):
638
+ await task
639
+ finally:
640
+ work_lease.release()
641
+
642
+ async def _serve_http11_request(
643
+ self,
644
+ reader: StreamReaderLike,
645
+ writer: asyncio.StreamWriter,
646
+ request: ParsedRequestHead,
647
+ *,
648
+ client: tuple[str, int] | None,
649
+ server: tuple[str, int] | tuple[str, None] | None,
650
+ scheme: str,
651
+ scope_extensions: dict | None = None,
652
+ ) -> bool:
653
+ host_header = get_header(request.headers, b'host')
654
+ if self.config.allowed_server_names and not authority_allowed(host_header, self.config.allowed_server_names):
655
+ await self._write_error(writer, 421, b'misdirected request', keep_alive=False)
656
+ return False
657
+ scope = build_http_scope(
658
+ request,
659
+ client=client,
660
+ server=server,
661
+ scheme=scheme,
662
+ extensions=self._http11_scope_extensions(request, scope_extensions=scope_extensions),
663
+ root_path=self.config.proxy.root_path,
664
+ proxy=self.config.proxy,
665
+ )
666
+ receive = self._build_http11_receive(reader, writer, request)
667
+ send = HTTPResponseCollector()
668
+ status = 500
669
+ trailers: list[tuple[bytes, bytes]] = []
670
+ try:
671
+ await self.app(scope, receive, send)
672
+ send.finalize()
673
+ assert send.status is not None
674
+ status = send.status
675
+ headers = list(send.headers)
676
+ trailers = list(send.trailers)
677
+ body = b''
678
+ body_segments = list(send.body_segments) if send.uses_streamed_body else None
679
+ for interim_status, interim_headers in send.informational_responses:
680
+ writer.write(
681
+ serialize_http11_response_head(
682
+ status=interim_status,
683
+ headers=interim_headers,
684
+ keep_alive=request.keep_alive,
685
+ server_header=self.config.server_header_value,
686
+ chunked=False,
687
+ include_date_header=self.config.include_date_header,
688
+ default_headers=self.config.default_response_headers,
689
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
690
+ )
691
+ )
692
+ if body_segments is None and send.has_spooled_body():
693
+ spooled_segments = send.spooled_body_segments()
694
+ spooled_path = ''
695
+ if spooled_segments:
696
+ first_segment = spooled_segments[0]
697
+ if isinstance(first_segment, FileBodySegment):
698
+ spooled_path = first_segment.path
699
+ planned = plan_file_backed_response_entity_semantics(
700
+ method=request.method,
701
+ request_headers=request.headers,
702
+ response_headers=headers,
703
+ status=status,
704
+ body_path=spooled_path,
705
+ body_length=send.body_length,
706
+ generated_etag=send.generated_entity_tag(),
707
+ apply_content_coding=True,
708
+ trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
709
+ )
710
+ if planned.requires_materialization:
711
+ body = await send.materialize_body()
712
+ processed = apply_response_entity_semantics(
713
+ method=request.method,
714
+ request_headers=request.headers,
715
+ response_headers=headers,
716
+ body=body,
717
+ status=status,
718
+ content_coding_policy=self.config.http.content_coding_policy,
719
+ supported_codings=tuple(self.config.http.content_codings),
720
+ apply_content_coding=True,
721
+ generate_etag=True,
722
+ trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
723
+ )
724
+ status = processed.status
725
+ headers = processed.headers
726
+ body = processed.body
727
+ if processed.head_response:
728
+ trailers = []
729
+ elif planned.use_body_segments:
730
+ status = planned.status
731
+ headers = planned.headers
732
+ body_segments = list(planned.body_segments)
733
+ body = b''
734
+ else:
735
+ status = planned.status
736
+ headers = planned.headers
737
+ body = planned.body
738
+ trailers = []
739
+ elif body_segments is None:
740
+ body = await send.materialize_body()
741
+ processed = apply_response_entity_semantics(
742
+ method=request.method,
743
+ request_headers=request.headers,
744
+ response_headers=headers,
745
+ body=body,
746
+ status=status,
747
+ content_coding_policy=self.config.http.content_coding_policy,
748
+ supported_codings=tuple(self.config.http.content_codings),
749
+ apply_content_coding=True,
750
+ generate_etag=True,
751
+ trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
752
+ )
753
+ status = processed.status
754
+ headers = processed.headers
755
+ body = processed.body
756
+ if processed.head_response:
757
+ trailers = []
758
+ if body_segments is None:
759
+ if trailers:
760
+ writer.write(
761
+ serialize_http11_response_head(
762
+ status=status,
763
+ headers=headers,
764
+ keep_alive=request.keep_alive,
765
+ server_header=self.config.server_header_value,
766
+ chunked=True,
767
+ include_date_header=self.config.include_date_header,
768
+ default_headers=self.config.default_response_headers,
769
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
770
+ )
771
+ )
772
+ if body:
773
+ writer.write(serialize_http11_response_chunk(body))
774
+ writer.write(finalize_chunked_body(trailers))
775
+ await self._drain_writer(writer)
776
+ else:
777
+ writer.write(
778
+ serialize_http11_response_whole(
779
+ status=status,
780
+ headers=headers,
781
+ body=body,
782
+ keep_alive=request.keep_alive,
783
+ server_header=self.config.server_header_value,
784
+ include_date_header=self.config.include_date_header,
785
+ default_headers=self.config.default_response_headers,
786
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version=request.http_version),
787
+ )
788
+ )
789
+ await self._drain_writer(writer)
790
+ else:
791
+ await self._send_http11_streamed_response(
792
+ writer,
793
+ request=request,
794
+ status=status,
795
+ headers=headers,
796
+ body_segments=body_segments,
797
+ trailers=trailers,
798
+ )
799
+ self.state.metrics.requests_served += 1
800
+ except ProtocolError:
801
+ self.state.metrics.requests_failed += 1
802
+ await self._write_error(writer, 400, b'bad request trailers', keep_alive=False)
803
+ return False
804
+ except Exception:
805
+ self.state.metrics.requests_failed += 1
806
+ self.logger.exception('application error')
807
+ await self._write_error(writer, 500, b'internal server error', keep_alive=False)
808
+ return False
809
+ finally:
810
+ send.cleanup()
811
+ self.access_logger.log_http(client, request.method, request.path, status, f'HTTP/{request.http_version}')
812
+ body_complete = getattr(receive, 'body_complete', True)
813
+ return request.keep_alive and body_complete
814
+
815
+ async def _write_error(
816
+ self,
817
+ writer: asyncio.StreamWriter,
818
+ status: int,
819
+ body: bytes,
820
+ *,
821
+ keep_alive: bool,
822
+ ) -> None:
823
+ writer.write(
824
+ serialize_http11_response_whole(
825
+ status=status,
826
+ headers=[(b'content-type', b'text/plain; charset=utf-8')],
827
+ body=body,
828
+ keep_alive=keep_alive,
829
+ server_header=self.config.server_header_value,
830
+ include_date_header=self.config.include_date_header,
831
+ default_headers=self.config.default_response_headers,
832
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version='1.1'),
833
+ )
834
+ )
835
+ await self._drain_writer(writer)
836
+
837
+ async def _start_metrics_endpoint(self, bind: str) -> asyncio.AbstractServer:
838
+ host, port = self._parse_bind_target(bind)
839
+ server = await asyncio.start_server(self._handle_metrics_request, host=host, port=port)
840
+ self.logger.info('metrics endpoint listening on %s', bind)
841
+ return server
842
+
843
+ async def _handle_metrics_request(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
844
+ with suppress(Exception):
845
+ await asyncio.wait_for(reader.readuntil(b'\r\n\r\n'), timeout=1.0)
846
+ payload = self.state.metrics.render_prometheus().encode('utf-8')
847
+ response = serialize_http11_response_whole(
848
+ status=200,
849
+ headers=[(b'content-type', b'text/plain; version=0.0.4')],
850
+ body=payload,
851
+ keep_alive=False,
852
+ server_header=self.config.server_header_value,
853
+ include_date_header=self.config.include_date_header,
854
+ default_headers=self.config.default_response_headers,
855
+ )
856
+ writer.write(response)
857
+ with suppress(Exception):
858
+ await writer.drain()
859
+ writer.close()
860
+ with suppress(Exception):
861
+ await writer.wait_closed()
862
+
863
+ @staticmethod
864
+ def _parse_bind_target(bind: str) -> tuple[str, int]:
865
+ if bind.startswith('[') and ']:' in bind:
866
+ host, port = bind.rsplit(':', 1)
867
+ return host[1:-1], int(port)
868
+ host, port = bind.rsplit(':', 1)
869
+ return host, int(port)
870
+
871
+ async def _monitor_request_budget(self) -> None:
872
+ assert self._request_budget is not None
873
+ while not self._should_exit.is_set() and not self.state.shutting_down:
874
+ if self.state.metrics.requests_served >= self._request_budget:
875
+ self.logger.info('request budget reached, shutting down worker')
876
+ self.request_shutdown()
877
+ return
878
+ await asyncio.sleep(0.1)