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.
- {tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime.egg-info → tigrcorn_runtime-0.3.17.dev1}/PKG-INFO +7 -7
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/pyproject.toml +7 -7
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/embedded.py +15 -1
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/__init__.py +5 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/base.py +49 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/client_handlers.py +134 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/core.py +28 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/diagnostics.py +150 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11.py +179 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11_send.py +153 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/http11_serve.py +209 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/imports.py +56 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/lifecycle.py +179 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/listeners.py +94 -0
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime/server/runner/metrics.py +59 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime.egg-info}/PKG-INFO +7 -7
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime.egg-info/SOURCES.txt +12 -1
- tigrcorn_runtime-0.3.17.dev1/src/tigrcorn_runtime.egg-info/requires.txt +14 -0
- tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime/server/runner.py +0 -881
- tigrcorn_runtime-0.3.16.dev13/src/tigrcorn_runtime.egg-info/requires.txt +0 -14
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/LICENSE +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/README.md +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/setup.cfg +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/__init__.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/api.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/app_interfaces.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/cli.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/py.typed +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/__init__.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/app_loader.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/bootstrap.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/hooks.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/reloader.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/shutdown.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/signals.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/state.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/server/supervisor.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/__init__.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/local.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/model.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/process.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/workers/supervisor.py +0 -0
- {tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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.
|
|
191
|
-
Requires-Dist: tigrcorn-config==0.3.
|
|
192
|
-
Requires-Dist: tigrcorn-asgi==0.3.
|
|
193
|
-
Requires-Dist: tigrcorn-transports==0.3.
|
|
194
|
-
Requires-Dist: tigrcorn-protocols==0.3.
|
|
195
|
-
Requires-Dist: tigrcorn-security==0.3.
|
|
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.
|
|
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.
|
|
34
|
-
"tigrcorn-config==0.3.
|
|
35
|
-
"tigrcorn-asgi==0.3.
|
|
36
|
-
"tigrcorn-transports==0.3.
|
|
37
|
-
"tigrcorn-protocols==0.3.
|
|
38
|
-
"tigrcorn-security==0.3.
|
|
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]
|
{tigrcorn_runtime-0.3.16.dev13 → tigrcorn_runtime-0.3.17.dev1}/src/tigrcorn_runtime/embedded.py
RENAMED
|
@@ -38,7 +38,9 @@ class EmbeddedServer:
|
|
|
38
38
|
async def close(self) -> None:
|
|
39
39
|
if self.server is None:
|
|
40
40
|
return
|
|
41
|
-
|
|
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,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('__')]
|