tigrcorn-runtime 0.3.16.dev13__tar.gz → 0.3.17.dev1__tar.gz

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.
Files changed (44) hide show
  1. {tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime.egg-info → tigrcorn_runtime-0.3.17.dev1}/PKG-INFO +7 -7
  2. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/pyproject.toml +7 -7
  3. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/embedded.py +15 -1
  4. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/__init__.py +5 -0
  5. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/base.py +49 -0
  6. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/client_handlers.py +134 -0
  7. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/core.py +28 -0
  8. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/diagnostics.py +150 -0
  9. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11.py +179 -0
  10. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11_send.py +153 -0
  11. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11_serve.py +209 -0
  12. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/imports.py +56 -0
  13. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/lifecycle.py +179 -0
  14. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/listeners.py +94 -0
  15. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/metrics.py +59 -0
  16. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime.egg-info}/PKG-INFO +7 -7
  17. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime.egg-info/SOURCES.txt +12 -1
  18. tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime.egg-info/requires.txt +14 -0
  19. tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime/server/runner.py +0 -881
  20. tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime.egg-info/requires.txt +0 -14
  21. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/LICENSE +0 -0
  22. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/README.md +0 -0
  23. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/setup.cfg +0 -0
  24. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/__init__.py +0 -0
  25. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/api.py +0 -0
  26. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/app_interfaces.py +0 -0
  27. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/cli.py +0 -0
  28. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/py.typed +0 -0
  29. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/__init__.py +0 -0
  30. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/app_loader.py +0 -0
  31. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/bootstrap.py +0 -0
  32. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/hooks.py +0 -0
  33. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/reloader.py +0 -0
  34. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/shutdown.py +0 -0
  35. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/signals.py +0 -0
  36. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/state.py +0 -0
  37. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/supervisor.py +0 -0
  38. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/__init__.py +0 -0
  39. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/local.py +0 -0
  40. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/model.py +0 -0
  41. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/process.py +0 -0
  42. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/supervisor.py +0 -0
  43. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime.egg-info/dependency_links.txt +0 -0
  44. {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrcorn-runtime
3
- Version: 0.3.16.dev13
3
+ Version: 0.3.17.dev1
4
4
  Summary: Server runner, app loading, lifecycle hooks, workers, reload, and embedded runtime for the Tigrcorn ASGI3 web server.
5
5
  Author-email: Jacob Stewart <jacob@swarmauri.com>
6
6
  License: Apache License
@@ -187,12 +187,12 @@ Classifier: Typing :: Typed
187
187
  Requires-Python: <3.15,>=3.10
188
188
  Description-Content-Type: text/markdown
189
189
  License-File: LICENSE
190
- Requires-Dist: tigrcorn-core==0.3.16.dev13
191
- Requires-Dist: tigrcorn-config==0.3.16.dev13
192
- Requires-Dist: tigrcorn-asgi==0.3.16.dev13
193
- Requires-Dist: tigrcorn-transports==0.3.16.dev13
194
- Requires-Dist: tigrcorn-protocols==0.3.16.dev13
195
- Requires-Dist: tigrcorn-security==0.3.16.dev13
190
+ Requires-Dist: tigrcorn-core==0.3.17.dev1
191
+ Requires-Dist: tigrcorn-config==0.3.17.dev1
192
+ Requires-Dist: tigrcorn-asgi==0.3.17.dev1
193
+ Requires-Dist: tigrcorn-transports==0.3.17.dev1
194
+ Requires-Dist: tigrcorn-protocols==0.3.17.dev1
195
+ Requires-Dist: tigrcorn-security==0.3.17.dev1
196
196
  Provides-Extra: uvloop
197
197
  Requires-Dist: uvloop>=0.19.0; platform_system != "Windows" and extra == "uvloop"
198
198
  Provides-Extra: trio
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tigrcorn-runtime"
7
- version = "0.3.16.dev13"
7
+ version = "0.3.17.dev1"
8
8
  description = "Server runner, app loading, lifecycle hooks, workers, reload, and embedded runtime for the Tigrcorn ASGI3 web server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<3.15"
@@ -30,12 +30,12 @@ classifiers = [
30
30
  "Typing :: Typed",
31
31
  ]
32
32
  dependencies = [
33
- "tigrcorn-core==0.3.16.dev13",
34
- "tigrcorn-config==0.3.16.dev13",
35
- "tigrcorn-asgi==0.3.16.dev13",
36
- "tigrcorn-transports==0.3.16.dev13",
37
- "tigrcorn-protocols==0.3.16.dev13",
38
- "tigrcorn-security==0.3.16.dev13",
33
+ "tigrcorn-core==0.3.17.dev1",
34
+ "tigrcorn-config==0.3.17.dev1",
35
+ "tigrcorn-asgi==0.3.17.dev1",
36
+ "tigrcorn-transports==0.3.17.dev1",
37
+ "tigrcorn-protocols==0.3.17.dev1",
38
+ "tigrcorn-security==0.3.17.dev1",
39
39
  ]
40
40
 
41
41
  [project.optional-dependencies]
@@ -38,7 +38,9 @@ class EmbeddedServer:
38
38
  async def close(self) -> None:
39
39
  if self.server is None:
40
40
  return
41
- await self.server.close()
41
+ server = self.server
42
+ await server.close()
43
+ self.server = None
42
44
 
43
45
  async def __aenter__(self) -> 'EmbeddedServer':
44
46
  await self.start()
@@ -73,5 +75,17 @@ class EmbeddedServer:
73
75
  endpoints.append(path)
74
76
  return endpoints
75
77
 
78
+ def describe(self) -> dict[str, Any]:
79
+ if self.server is not None:
80
+ return self.server.describe()
81
+ server = TigrCornServer(self.app, self.config)
82
+ return server.describe()
83
+
84
+ def resource_ownership(self) -> dict[str, Any]:
85
+ if self.server is not None:
86
+ return self.server.resource_ownership()
87
+ server = TigrCornServer(self.app, self.config)
88
+ return server.resource_ownership()
89
+
76
90
 
77
91
  __all__ = ['EmbeddedServer']
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .core import TigrCornServer
4
+
5
+ __all__ = ['TigrCornServer']
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from .imports import *
4
+
5
+ class _TigrCornServerBase:
6
+ def __init__(self, app: ASGIApp, config: ServerConfig) -> None:
7
+ selection = resolve_app_dispatch(app, config.app.interface)
8
+ self.app = selection.app
9
+ self.app_dispatch_selection = selection
10
+ self.app_interface = selection.interface
11
+ self.app_interface_source = selection.source
12
+ self.app_interface_reason = selection.reason
13
+ self.config = config
14
+ self._resolved_logging = resolve_logging_config(config.log_level, config=config.logging)
15
+ self.logger = configure_logging(config.log_level, config=config.logging)
16
+ self.access_logger = AccessLogger(
17
+ self.logger,
18
+ enabled=self._resolved_logging.access_log,
19
+ fmt=self._resolved_logging.access_log_format,
20
+ )
21
+ self.state = ServerState()
22
+ self.lifespan = LifespanManager(app, mode=config.lifespan)
23
+ self._listeners: list[TCPListener | UDPListener | UnixListener | PipeListener | InProcListener] = []
24
+ self._datagram_handlers: list[HTTP3DatagramHandler] = []
25
+ self._transport_domain_accounting = TransportDomainAccounting()
26
+ self._quic_operational_security: dict[str, QuicOperationalSecurityRuntime] = {}
27
+ self._webtransport_governance = WebTransportGovernanceManager(default_webtransport_budget_policy())
28
+ self._should_exit = asyncio.Event()
29
+ self._started = False
30
+ self._metrics_server: asyncio.AbstractServer | None = None
31
+ self._request_budget_task: asyncio.Task[None] | None = None
32
+ self._statsd_exporter = StatsdExporter(config.metrics.statsd_host, logger=self.logger) if config.metrics.statsd_host else None
33
+ self._otel_exporter = OtelExporter(config.metrics.otel_endpoint, logger=self.logger) if config.metrics.otel_endpoint else None
34
+ policy = SchedulerPolicy()
35
+ if config.scheduler.max_connections is not None:
36
+ policy.max_connections = config.scheduler.max_connections
37
+ if config.scheduler.max_tasks is not None:
38
+ policy.max_tasks = config.scheduler.max_tasks
39
+ if config.scheduler.max_streams is not None:
40
+ policy.max_streams_per_session = config.scheduler.max_streams
41
+ if config.scheduler.limit_concurrency is not None:
42
+ policy.limit_concurrency = config.scheduler.limit_concurrency
43
+ self.scheduler = ProductionScheduler(policy)
44
+ self._request_budget = None
45
+ if config.process.limit_max_requests is not None:
46
+ jitter = max(0, config.process.max_requests_jitter)
47
+ self._request_budget = config.process.limit_max_requests + (random.randint(0, jitter) if jitter else 0)
48
+
49
+ __all__ = [name for name in globals() if not name.startswith('__')]
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from .imports import *
4
+
5
+ class _TigrCornServerClientHandlerMixin:
6
+ def _make_client_handler(self, listener_cfg: ListenerConfig):
7
+ if listener_cfg.kind == 'udp':
8
+ h3_handler = HTTP3DatagramHandler(
9
+ app=self.app,
10
+ config=self.config,
11
+ listener=listener_cfg,
12
+ access_logger=self.access_logger,
13
+ scheduler=self.scheduler,
14
+ metrics=self.state.metrics,
15
+ webtransport_governance=self._webtransport_governance,
16
+ )
17
+ self._datagram_handlers.append(h3_handler)
18
+
19
+ async def udp_handler(packet, endpoint) -> None:
20
+ sessions_before = len(h3_handler.sessions)
21
+ responses_before = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
22
+ self._record_quic_operational_security_packet(listener_cfg, packet, endpoint)
23
+ await h3_handler.handle_packet(packet, endpoint)
24
+ if len(h3_handler.sessions) > sessions_before:
25
+ self.state.metrics.connection_opened()
26
+ responses_after = sum(len(session.responded_streams) for session in h3_handler.sessions.values())
27
+ if responses_after > responses_before:
28
+ self.state.metrics.requests_served += responses_after - responses_before
29
+
30
+ return udp_handler
31
+
32
+ if listener_cfg.kind == 'pipe':
33
+ raw_handler = RawFramedApplicationHandler(
34
+ app=self.app,
35
+ config=self.config,
36
+ listener=listener_cfg,
37
+ access_logger=self.access_logger,
38
+ )
39
+
40
+ async def pipe_handler(connection, data) -> None:
41
+ handled = await raw_handler.feed_bytes(connection, data, path=listener_cfg.path)
42
+ self.state.metrics.requests_served += handled
43
+ self.state.metrics.bytes_received += len(data)
44
+
45
+ return pipe_handler
46
+
47
+ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
48
+ await self._handle_client(reader, writer, listener_cfg)
49
+
50
+ return handler
51
+
52
+ async def _handle_client(
53
+ self,
54
+ reader: asyncio.StreamReader,
55
+ writer: asyncio.StreamWriter,
56
+ listener_cfg: ListenerConfig,
57
+ ) -> None:
58
+ lease = self.scheduler.acquire_connection()
59
+ if lease is None:
60
+ writer.close()
61
+ with suppress(Exception):
62
+ await writer.wait_closed()
63
+ return
64
+ self.state.metrics.connection_opened()
65
+ peername = writer.get_extra_info('peername')
66
+ sockname = writer.get_extra_info('sockname')
67
+ ssl_obj = writer.get_extra_info('ssl_object')
68
+ selected_alpn = ssl_obj.selected_alpn_protocol() if ssl_obj else None
69
+ tls_payload = tls_extension_payload(writer)
70
+ scope_tls_extensions = {'tls': tls_payload} if tls_payload is not None else None
71
+ client_host, client_port = peer_parts(peername)
72
+ server_host, server_port = peer_parts(sockname)
73
+ client = (client_host, client_port) if client_host is not None and client_port is not None else None
74
+ server = (server_host or '', server_port)
75
+ scheme = 'https' if ssl_obj else (listener_cfg.scheme or 'http')
76
+ ws_scheme = 'wss' if ssl_obj else 'ws'
77
+ try:
78
+ if selected_alpn == 'h2' and '2' in listener_cfg.http_versions:
79
+ h2_handler = HTTP2ConnectionHandler(
80
+ app=self.app,
81
+ config=self.config,
82
+ access_logger=self.access_logger,
83
+ scheduler=self.scheduler,
84
+ metrics=self.state.metrics,
85
+ reader=reader,
86
+ writer=writer,
87
+ client=client,
88
+ server=server,
89
+ scheme=scheme,
90
+ scope_extensions=scope_tls_extensions,
91
+ )
92
+ await h2_handler.handle()
93
+ return
94
+
95
+ initial = b''
96
+ if '2' in listener_cfg.http_versions and self.config.enable_h2c:
97
+ initial = await self._read_preface_probe(reader)
98
+ if initial == H2_PREFACE:
99
+ h2_handler = HTTP2ConnectionHandler(
100
+ app=self.app,
101
+ config=self.config,
102
+ access_logger=self.access_logger,
103
+ scheduler=self.scheduler,
104
+ metrics=self.state.metrics,
105
+ reader=reader,
106
+ writer=writer,
107
+ client=client,
108
+ server=server,
109
+ scheme=scheme,
110
+ prebuffer=initial,
111
+ scope_extensions=scope_tls_extensions,
112
+ )
113
+ await h2_handler.handle()
114
+ return
115
+
116
+ buffered_reader: StreamReaderLike = PrebufferedReader(reader, initial)
117
+ await self._handle_http11_connection(
118
+ buffered_reader,
119
+ writer,
120
+ listener_cfg,
121
+ client=client,
122
+ server=server,
123
+ scheme=scheme,
124
+ ws_scheme=ws_scheme,
125
+ scope_extensions=scope_tls_extensions,
126
+ )
127
+ finally:
128
+ lease.release()
129
+ self.state.metrics.connection_closed()
130
+ writer.close()
131
+ with suppress(Exception):
132
+ await writer.wait_closed()
133
+
134
+ __all__ = [name for name in globals() if not name.startswith('__')]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import _TigrCornServerBase
4
+ from .diagnostics import _TigrCornServerDiagnosticsMixin
5
+ from .lifecycle import _TigrCornServerLifecycleMixin
6
+ from .listeners import _TigrCornServerListenerMixin
7
+ from .client_handlers import _TigrCornServerClientHandlerMixin
8
+ from .http11 import _TigrCornServerHTTP11Mixin
9
+ from .http11_send import _TigrCornServerHTTP11SendMixin
10
+ from .http11_serve import _TigrCornServerHTTP11ServeMixin
11
+ from .metrics import _TigrCornServerMetricsMixin
12
+
13
+
14
+ class TigrCornServer(
15
+ _TigrCornServerMetricsMixin,
16
+ _TigrCornServerHTTP11ServeMixin,
17
+ _TigrCornServerHTTP11SendMixin,
18
+ _TigrCornServerHTTP11Mixin,
19
+ _TigrCornServerClientHandlerMixin,
20
+ _TigrCornServerListenerMixin,
21
+ _TigrCornServerLifecycleMixin,
22
+ _TigrCornServerDiagnosticsMixin,
23
+ _TigrCornServerBase,
24
+ ):
25
+ pass
26
+
27
+
28
+ __all__ = ['TigrCornServer']
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from .imports import *
4
+
5
+ class _TigrCornServerDiagnosticsMixin:
6
+ def describe(self) -> dict[str, Any]:
7
+ configured_protocols: set[str] = set()
8
+ configured_transports: set[str] = set()
9
+ listeners: list[dict[str, Any]] = []
10
+ bound_endpoints = self._bound_endpoint_strings()
11
+ for index, listener in enumerate(self.config.listeners):
12
+ protocols = tuple(sorted(listener.enabled_protocols))
13
+ configured_protocols.update(protocols)
14
+ configured_transports.add(listener.kind)
15
+ listeners.append(
16
+ {
17
+ "id": f"listener:{index}",
18
+ "active": self._started and index < len(self._listeners),
19
+ "kind": listener.kind,
20
+ "label": listener.label,
21
+ "bound_endpoint": bound_endpoints[index] if index < len(bound_endpoints) else None,
22
+ "protocols": list(protocols),
23
+ "http_versions": list(listener.http_versions),
24
+ "tls": {
25
+ "enabled": listener.ssl_enabled,
26
+ "alpn_protocols": list(listener.alpn_protocols),
27
+ "certfile": self._redact(listener.ssl_certfile),
28
+ "keyfile": self._redact(listener.ssl_keyfile),
29
+ "ca_certs": self._redact(listener.ssl_ca_certs),
30
+ "client_cert_required": bool(listener.ssl_require_client_cert),
31
+ "ocsp_mode": listener.ocsp_mode,
32
+ "crl_mode": listener.crl_mode,
33
+ },
34
+ }
35
+ )
36
+ return {
37
+ "schema_version": "1.0",
38
+ "runtime": self.config.process.runtime,
39
+ "active": self._started,
40
+ "profile": self.config.app.profile or "default",
41
+ "app_interface": {
42
+ "selected": self.app_interface,
43
+ "source": self.app_interface_source,
44
+ "reason": self.app_interface_reason,
45
+ },
46
+ "capabilities": {
47
+ "protocols": ["http1", "http2", "http3", "lifespan", "quic", "websocket", "webtransport"],
48
+ "transports": sorted(TRANSPORTS),
49
+ },
50
+ "configured_protocols": sorted(configured_protocols),
51
+ "active_protocols": sorted(configured_protocols) if self._started else [],
52
+ "configured_transports": sorted(configured_transports),
53
+ "active_transports": sorted(configured_transports) if self._started else [],
54
+ "transport_domains": self.transport_domain_diagnostics(),
55
+ "quic_operational_security": self.quic_operational_security_evidence(),
56
+ "webtransport_resource_governance": self.webtransport_resource_governance(),
57
+ "listeners": listeners,
58
+ "worker": {
59
+ "count": self.config.process.workers,
60
+ "class": self.config.process.worker_class,
61
+ "runtime": self.config.process.runtime,
62
+ },
63
+ "observability": {
64
+ "metrics_enabled": bool(self.config.metrics.enabled),
65
+ "metrics_bind": self.config.metrics.bind,
66
+ "statsd_enabled": bool(self.config.metrics.statsd_host),
67
+ "otel_enabled": bool(self.config.metrics.otel_endpoint),
68
+ "structured_logging": bool(self.config.logging.structured),
69
+ },
70
+ }
71
+
72
+ def _bound_endpoint_strings(self) -> list[str]:
73
+ endpoints: list[str] = []
74
+ for listener in self._listeners:
75
+ server = getattr(listener, "server", None)
76
+ sockets = getattr(server, "sockets", None) if server is not None else None
77
+ if sockets:
78
+ endpoints.append(str(sockets[0].getsockname()))
79
+ continue
80
+ transport = getattr(listener, "transport", None)
81
+ if transport is not None:
82
+ sockname = transport.get_extra_info("sockname")
83
+ if sockname is not None:
84
+ endpoints.append(str(sockname))
85
+ continue
86
+ path = getattr(listener, "path", None)
87
+ if path:
88
+ endpoints.append(str(path))
89
+ return endpoints
90
+
91
+ def transport_domain_diagnostics(self) -> dict[str, Any]:
92
+ active_domains = self._active_transport_domain_ids() if self._started else ()
93
+ return transport_domain_diagnostics(
94
+ accounting=self._transport_domain_accounting,
95
+ active_domains=active_domains,
96
+ endpoint_identities=self._transport_domain_endpoint_identities(active_domains),
97
+ )
98
+
99
+ def quic_operational_security_evidence(self) -> dict[str, Any]:
100
+ return {
101
+ label: collector.runtime_evidence()
102
+ for label, collector in sorted(self._quic_operational_security.items())
103
+ }
104
+
105
+ def webtransport_resource_governance(self) -> dict[str, Any]:
106
+ return self._jsonable(self._webtransport_governance.snapshot())
107
+
108
+ def _active_transport_domain_ids(self) -> tuple[str, ...]:
109
+ domains: set[str] = set()
110
+ for index, listener in enumerate(self.config.listeners):
111
+ if index >= len(self._listeners):
112
+ continue
113
+ domains.update(self._listener_transport_domain_ids(listener))
114
+ return tuple(sorted(domains))
115
+
116
+ def _configured_transport_domain_ids(self) -> tuple[str, ...]:
117
+ domains: set[str] = set()
118
+ for listener in self.config.listeners:
119
+ domains.update(self._listener_transport_domain_ids(listener))
120
+ return tuple(sorted(domains))
121
+
122
+ @staticmethod
123
+ def _listener_transport_domain_ids(listener: ListenerConfig) -> tuple[str, ...]:
124
+ domains = {"listener", _normalize_listener_kind(listener.kind)}
125
+ if "quic" in listener.enabled_protocols:
126
+ domains.add("quic")
127
+ return tuple(sorted(domains))
128
+
129
+ def _transport_domain_endpoint_identities(self, active_domains: Iterable[str]) -> dict[str, str | None]:
130
+ endpoint = ",".join(self._bound_endpoint_strings()) or None
131
+ return {domain_id: endpoint for domain_id in active_domains}
132
+
133
+ @staticmethod
134
+ def _redact(value: Any) -> str | None:
135
+ return "<redacted>" if value else None
136
+
137
+ @staticmethod
138
+ def _jsonable(value: Any) -> Any:
139
+ if isinstance(value, tuple):
140
+ return [_TigrCornServerDiagnosticsMixin._jsonable(item) for item in value]
141
+ if isinstance(value, list):
142
+ return [_TigrCornServerDiagnosticsMixin._jsonable(item) for item in value]
143
+ if isinstance(value, dict):
144
+ return {
145
+ key: _TigrCornServerDiagnosticsMixin._jsonable(item)
146
+ for key, item in value.items()
147
+ }
148
+ return value
149
+
150
+ __all__ = [name for name in globals() if not name.startswith('__')]
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ from .imports import *
4
+
5
+ class _TigrCornServerHTTP11Mixin:
6
+ async def _read_preface_probe(self, reader: asyncio.StreamReader) -> bytes:
7
+ data = await asyncio.wait_for(reader.read(len(H2_PREFACE)), timeout=self.config.http.read_timeout)
8
+ if not data:
9
+ return b''
10
+ if H2_PREFACE.startswith(data) and data != H2_PREFACE:
11
+ with suppress(Exception):
12
+ data += await asyncio.wait_for(reader.readexactly(len(H2_PREFACE) - len(data)), timeout=0.05)
13
+ return data
14
+
15
+ async def _handle_http11_connection(
16
+ self,
17
+ reader: StreamReaderLike,
18
+ writer: asyncio.StreamWriter,
19
+ listener_cfg: ListenerConfig,
20
+ *,
21
+ client: tuple[str, int] | None,
22
+ server: tuple[str, int] | tuple[str, None] | None,
23
+ scheme: str,
24
+ ws_scheme: str,
25
+ scope_extensions: dict | None = None,
26
+ ) -> None:
27
+ keep_handling = True
28
+ handled_requests = 0
29
+ while keep_handling and not self.state.shutting_down:
30
+ request_timeout = self.config.http.keep_alive_timeout if handled_requests else self.config.http.read_timeout
31
+ if self.config.http.http1_header_read_timeout is not None:
32
+ request_timeout = min(request_timeout, self.config.http.http1_header_read_timeout)
33
+ try:
34
+ request = await asyncio.wait_for(
35
+ read_http11_request_head(
36
+ reader,
37
+ max_body_size=self.config.max_body_size,
38
+ max_header_size=self.config.max_header_size,
39
+ max_incomplete_event_size=self.config.http.http1_max_incomplete_event_size,
40
+ buffer_size=self.config.http.http1_buffer_size,
41
+ ),
42
+ timeout=request_timeout,
43
+ )
44
+ except asyncio.TimeoutError:
45
+ break
46
+ except Exception as exc:
47
+ self.state.metrics.protocol_errors += 1
48
+ self.logger.warning('protocol error from %s: %s', client, exc)
49
+ await self._write_error(writer, 400, b'bad request', keep_alive=False)
50
+ break
51
+ if request is None:
52
+ break
53
+
54
+ proxy_view = resolve_proxy_view(
55
+ request.headers,
56
+ client=client,
57
+ server=server,
58
+ scheme=scheme,
59
+ root_path=self.config.proxy.root_path,
60
+ enabled=self.config.proxy.proxy_headers,
61
+ forwarded_allow_ips=self.config.proxy.forwarded_allow_ips,
62
+ )
63
+ request_client = proxy_view.client
64
+ request_server = proxy_view.server
65
+ request_scheme = proxy_view.scheme
66
+ request_ws_scheme = 'wss' if request_scheme == 'https' else 'ws'
67
+ request.keep_alive = apply_keep_alive_policy(request.keep_alive, enabled=self.config.http.http1_keep_alive)
68
+
69
+ if request.method.upper() == 'CONNECT':
70
+ await self._handle_http11_connect_tunnel(reader, writer, request, client=request_client)
71
+ keep_handling = False
72
+ break
73
+
74
+ if request.websocket_upgrade:
75
+ if not listener_cfg.websocket:
76
+ await self._write_error(writer, 426, b'websocket not enabled', keep_alive=False)
77
+ break
78
+ work_lease = self.scheduler.acquire_work()
79
+ if work_lease is None:
80
+ self.state.metrics.scheduler_task_rejected()
81
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
82
+ break
83
+ handler = WebSocketConnectionHandler(
84
+ app=self.app,
85
+ config=self.config,
86
+ access_logger=self.access_logger,
87
+ request=request,
88
+ reader=reader,
89
+ writer=writer,
90
+ client=request_client,
91
+ server=request_server,
92
+ scheme=request_ws_scheme,
93
+ scope_extensions=scope_extensions,
94
+ metrics=self.state.metrics,
95
+ )
96
+ try:
97
+ self.state.metrics.websocket_opened()
98
+ await handler.handle()
99
+ finally:
100
+ work_lease.release()
101
+ self.state.metrics.websocket_closed()
102
+ keep_handling = False
103
+ break
104
+
105
+ work_lease = self.scheduler.acquire_work()
106
+ if work_lease is None:
107
+ self.state.metrics.scheduler_task_rejected()
108
+ await self._write_error(writer, 503, b'scheduler overloaded', keep_alive=False)
109
+ break
110
+ try:
111
+ keep_handling = await self._serve_http11_request(
112
+ reader,
113
+ writer,
114
+ request,
115
+ client=request_client,
116
+ server=request_server,
117
+ scheme=request_scheme,
118
+ scope_extensions=scope_extensions,
119
+ )
120
+ finally:
121
+ work_lease.release()
122
+ handled_requests += 1
123
+
124
+ async def _drain_writer(self, writer: asyncio.StreamWriter) -> None:
125
+ await asyncio.wait_for(writer.drain(), timeout=self.config.http.write_timeout)
126
+
127
+ async def _write_continue(self, writer: asyncio.StreamWriter) -> None:
128
+ writer.write(b'HTTP/1.1 100 Continue\r\n\r\n')
129
+ await self._drain_writer(writer)
130
+
131
+ def _build_http11_receive(
132
+ self,
133
+ reader: StreamReaderLike,
134
+ writer: asyncio.StreamWriter,
135
+ request: ParsedRequestHead,
136
+ ) -> HTTPRequestReceive | HTTPStreamingRequestReceive:
137
+ if request.body_kind == 'none':
138
+ return HTTPRequestReceive(b'')
139
+ return HTTPStreamingRequestReceive(
140
+ reader=reader,
141
+ content_length=request.content_length if request.body_kind == 'content-length' else None,
142
+ chunked=request.body_kind == 'chunked',
143
+ max_body_size=self.config.max_body_size,
144
+ expect_continue=request.expect_continue,
145
+ on_expect_continue=lambda: self._write_continue(writer),
146
+ max_chunk_size=self.config.http.http1_buffer_size,
147
+ trailer_policy=self.config.http.trailer_policy,
148
+ )
149
+
150
+ def _http11_scope_extensions(self, request: ParsedRequestHead, *, scope_extensions: dict | None = None) -> dict:
151
+ extensions: dict = dict(scope_extensions or {})
152
+ if request.body_kind == 'chunked' and self.config.http.trailer_policy != 'drop':
153
+ extensions['tigrcorn.http.request_trailers'] = {}
154
+ if request.method.upper() == 'CONNECT':
155
+ extensions['tigrcorn.http.connect'] = {'authority': request.target}
156
+ extensions['tigrcorn.http.response.file'] = {'protocol': 'http/1.1', 'streaming': True, 'sendfile': True}
157
+ extensions['http.response.pathsend'] = {}
158
+ return extensions
159
+
160
+ @staticmethod
161
+ @staticmethod
162
+ def _parse_connect_authority(authority: str) -> tuple[str, int]:
163
+ return parse_connect_authority(authority)
164
+
165
+ async def _relay_stream(self, reader: StreamReaderLike, writer: asyncio.StreamWriter) -> None:
166
+ try:
167
+ while True:
168
+ chunk = await asyncio.wait_for(reader.read(65536), timeout=self.config.http.idle_timeout)
169
+ if not chunk:
170
+ break
171
+ writer.write(chunk)
172
+ await self._drain_writer(writer)
173
+ self.state.metrics.bytes_sent += len(chunk)
174
+ finally:
175
+ writer.close()
176
+ with suppress(Exception):
177
+ await writer.wait_closed()
178
+
179
+ __all__ = [name for name in globals() if not name.startswith('__')]