ominfra 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev124__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.
- ominfra/deploy/_executor.py +7 -4
- ominfra/deploy/poly/_main.py +2 -4
- ominfra/pyremote/_runcommands.py +7 -4
- ominfra/scripts/journald2aws.py +7 -4
- ominfra/scripts/supervisor.py +1104 -13
- ominfra/supervisor/LICENSE.txt +28 -0
- ominfra/supervisor/inject.py +81 -0
- ominfra/supervisor/main.py +9 -74
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/METADATA +7 -8
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/RECORD +14 -12
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/WHEEL +1 -1
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev123.dist-info → ominfra-0.0.0.dev124.dist-info}/top_level.txt +0 -0
ominfra/scripts/supervisor.py
CHANGED
@@ -12,13 +12,18 @@ import ctypes as ct
|
|
12
12
|
import dataclasses as dc
|
13
13
|
import datetime
|
14
14
|
import decimal
|
15
|
+
import email.utils
|
15
16
|
import enum
|
16
17
|
import errno
|
17
18
|
import fcntl
|
18
19
|
import fractions
|
19
20
|
import functools
|
20
21
|
import grp
|
22
|
+
import html
|
23
|
+
import http.client
|
24
|
+
import http.server
|
21
25
|
import inspect
|
26
|
+
import io
|
22
27
|
import itertools
|
23
28
|
import json
|
24
29
|
import logging
|
@@ -30,11 +35,13 @@ import resource
|
|
30
35
|
import select
|
31
36
|
import shlex
|
32
37
|
import signal
|
38
|
+
import socket
|
33
39
|
import stat
|
34
40
|
import string
|
35
41
|
import sys
|
36
42
|
import syslog
|
37
43
|
import tempfile
|
44
|
+
import textwrap
|
38
45
|
import threading
|
39
46
|
import time
|
40
47
|
import traceback
|
@@ -49,8 +56,7 @@ import weakref # noqa
|
|
49
56
|
|
50
57
|
|
51
58
|
if sys.version_info < (3, 8):
|
52
|
-
raise OSError(
|
53
|
-
f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
|
59
|
+
raise OSError(f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
|
54
60
|
|
55
61
|
|
56
62
|
########################################
|
@@ -64,6 +70,13 @@ TomlPos = int # ta.TypeAlias
|
|
64
70
|
# ../../../omlish/lite/cached.py
|
65
71
|
T = ta.TypeVar('T')
|
66
72
|
|
73
|
+
# ../../../omlish/lite/socket.py
|
74
|
+
SocketAddress = ta.Any
|
75
|
+
SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'SocketHandler']
|
76
|
+
|
77
|
+
# ../../../omlish/lite/http/parsing.py
|
78
|
+
HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
|
79
|
+
|
67
80
|
# ../../../omlish/lite/inject.py
|
68
81
|
InjectorKeyCls = ta.Union[type, ta.NewType]
|
69
82
|
InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
|
@@ -73,6 +86,12 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
|
|
73
86
|
# ../../configs.py
|
74
87
|
ConfigMapping = ta.Mapping[str, ta.Any]
|
75
88
|
|
89
|
+
# ../../../omlish/lite/http/handlers.py
|
90
|
+
HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse']
|
91
|
+
|
92
|
+
# ../../../omlish/lite/http/coroserver.py
|
93
|
+
CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
|
94
|
+
|
76
95
|
# ../context.py
|
77
96
|
ServerEpoch = ta.NewType('ServerEpoch', int)
|
78
97
|
|
@@ -1254,6 +1273,11 @@ def check_not_isinstance(v: T, spec: ta.Union[type, tuple]) -> T:
|
|
1254
1273
|
return v
|
1255
1274
|
|
1256
1275
|
|
1276
|
+
def check_none(v: T) -> None:
|
1277
|
+
if v is not None:
|
1278
|
+
raise ValueError(v)
|
1279
|
+
|
1280
|
+
|
1257
1281
|
def check_not_none(v: ta.Optional[T]) -> T:
|
1258
1282
|
if v is None:
|
1259
1283
|
raise ValueError
|
@@ -1294,6 +1318,25 @@ def check_single(vs: ta.Iterable[T]) -> T:
|
|
1294
1318
|
return v
|
1295
1319
|
|
1296
1320
|
|
1321
|
+
########################################
|
1322
|
+
# ../../../omlish/lite/http/versions.py
|
1323
|
+
|
1324
|
+
|
1325
|
+
class HttpProtocolVersion(ta.NamedTuple):
|
1326
|
+
major: int
|
1327
|
+
minor: int
|
1328
|
+
|
1329
|
+
def __str__(self) -> str:
|
1330
|
+
return f'HTTP/{self.major}.{self.minor}'
|
1331
|
+
|
1332
|
+
|
1333
|
+
class HttpProtocolVersions:
|
1334
|
+
HTTP_0_9 = HttpProtocolVersion(0, 9)
|
1335
|
+
HTTP_1_0 = HttpProtocolVersion(1, 0)
|
1336
|
+
HTTP_1_1 = HttpProtocolVersion(1, 1)
|
1337
|
+
HTTP_2_0 = HttpProtocolVersion(2, 0)
|
1338
|
+
|
1339
|
+
|
1297
1340
|
########################################
|
1298
1341
|
# ../../../omlish/lite/json.py
|
1299
1342
|
|
@@ -1424,6 +1467,76 @@ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
|
|
1424
1467
|
todo.extend(reversed(cur.__subclasses__()))
|
1425
1468
|
|
1426
1469
|
|
1470
|
+
########################################
|
1471
|
+
# ../../../omlish/lite/socket.py
|
1472
|
+
"""
|
1473
|
+
TODO:
|
1474
|
+
- SocketClientAddress family / tuple pairs
|
1475
|
+
+ codification of https://docs.python.org/3/library/socket.html#socket-families
|
1476
|
+
"""
|
1477
|
+
|
1478
|
+
|
1479
|
+
##
|
1480
|
+
|
1481
|
+
|
1482
|
+
@dc.dataclass(frozen=True)
|
1483
|
+
class SocketAddressInfoArgs:
|
1484
|
+
host: ta.Optional[str]
|
1485
|
+
port: ta.Union[str, int, None]
|
1486
|
+
family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC
|
1487
|
+
type: int = 0
|
1488
|
+
proto: int = 0
|
1489
|
+
flags: socket.AddressInfo = socket.AddressInfo(0)
|
1490
|
+
|
1491
|
+
|
1492
|
+
@dc.dataclass(frozen=True)
|
1493
|
+
class SocketAddressInfo:
|
1494
|
+
family: socket.AddressFamily
|
1495
|
+
type: int
|
1496
|
+
proto: int
|
1497
|
+
canonname: ta.Optional[str]
|
1498
|
+
sockaddr: SocketAddress
|
1499
|
+
|
1500
|
+
|
1501
|
+
def get_best_socket_family(
|
1502
|
+
host: ta.Optional[str],
|
1503
|
+
port: ta.Union[str, int, None],
|
1504
|
+
family: ta.Union[int, socket.AddressFamily] = socket.AddressFamily.AF_UNSPEC,
|
1505
|
+
) -> ta.Tuple[socket.AddressFamily, SocketAddress]:
|
1506
|
+
"""https://github.com/python/cpython/commit/f289084c83190cc72db4a70c58f007ec62e75247"""
|
1507
|
+
|
1508
|
+
infos = socket.getaddrinfo(
|
1509
|
+
host,
|
1510
|
+
port,
|
1511
|
+
family,
|
1512
|
+
type=socket.SOCK_STREAM,
|
1513
|
+
flags=socket.AI_PASSIVE,
|
1514
|
+
)
|
1515
|
+
ai = SocketAddressInfo(*next(iter(infos)))
|
1516
|
+
return ai.family, ai.sockaddr
|
1517
|
+
|
1518
|
+
|
1519
|
+
##
|
1520
|
+
|
1521
|
+
|
1522
|
+
class SocketHandler(abc.ABC):
|
1523
|
+
def __init__(
|
1524
|
+
self,
|
1525
|
+
client_address: SocketAddress,
|
1526
|
+
rfile: ta.BinaryIO,
|
1527
|
+
wfile: ta.BinaryIO,
|
1528
|
+
) -> None:
|
1529
|
+
super().__init__()
|
1530
|
+
|
1531
|
+
self._client_address = client_address
|
1532
|
+
self._rfile = rfile
|
1533
|
+
self._wfile = wfile
|
1534
|
+
|
1535
|
+
@abc.abstractmethod
|
1536
|
+
def handle(self) -> None:
|
1537
|
+
raise NotImplementedError
|
1538
|
+
|
1539
|
+
|
1427
1540
|
########################################
|
1428
1541
|
# ../events.py
|
1429
1542
|
|
@@ -1910,6 +2023,371 @@ def timeslice(period: int, when: float) -> int:
|
|
1910
2023
|
return int(when - (when % period))
|
1911
2024
|
|
1912
2025
|
|
2026
|
+
########################################
|
2027
|
+
# ../../../omlish/lite/http/parsing.py
|
2028
|
+
|
2029
|
+
|
2030
|
+
##
|
2031
|
+
|
2032
|
+
|
2033
|
+
class ParseHttpRequestResult(abc.ABC): # noqa
|
2034
|
+
__slots__ = (
|
2035
|
+
'server_version',
|
2036
|
+
'request_line',
|
2037
|
+
'request_version',
|
2038
|
+
'version',
|
2039
|
+
'headers',
|
2040
|
+
'close_connection',
|
2041
|
+
)
|
2042
|
+
|
2043
|
+
def __init__(
|
2044
|
+
self,
|
2045
|
+
*,
|
2046
|
+
server_version: HttpProtocolVersion,
|
2047
|
+
request_line: str,
|
2048
|
+
request_version: HttpProtocolVersion,
|
2049
|
+
version: HttpProtocolVersion,
|
2050
|
+
headers: ta.Optional[HttpHeaders],
|
2051
|
+
close_connection: bool,
|
2052
|
+
) -> None:
|
2053
|
+
super().__init__()
|
2054
|
+
|
2055
|
+
self.server_version = server_version
|
2056
|
+
self.request_line = request_line
|
2057
|
+
self.request_version = request_version
|
2058
|
+
self.version = version
|
2059
|
+
self.headers = headers
|
2060
|
+
self.close_connection = close_connection
|
2061
|
+
|
2062
|
+
def __repr__(self) -> str:
|
2063
|
+
return f'{self.__class__.__name__}({", ".join(f"{a}={getattr(self, a)!r}" for a in self.__slots__)})'
|
2064
|
+
|
2065
|
+
|
2066
|
+
class EmptyParsedHttpResult(ParseHttpRequestResult):
|
2067
|
+
pass
|
2068
|
+
|
2069
|
+
|
2070
|
+
class ParseHttpRequestError(ParseHttpRequestResult):
|
2071
|
+
__slots__ = (
|
2072
|
+
'code',
|
2073
|
+
'message',
|
2074
|
+
*ParseHttpRequestResult.__slots__,
|
2075
|
+
)
|
2076
|
+
|
2077
|
+
def __init__(
|
2078
|
+
self,
|
2079
|
+
*,
|
2080
|
+
code: http.HTTPStatus,
|
2081
|
+
message: ta.Union[str, ta.Tuple[str, str]],
|
2082
|
+
|
2083
|
+
**kwargs: ta.Any,
|
2084
|
+
) -> None:
|
2085
|
+
super().__init__(**kwargs)
|
2086
|
+
|
2087
|
+
self.code = code
|
2088
|
+
self.message = message
|
2089
|
+
|
2090
|
+
|
2091
|
+
class ParsedHttpRequest(ParseHttpRequestResult):
|
2092
|
+
__slots__ = (
|
2093
|
+
'method',
|
2094
|
+
'path',
|
2095
|
+
'headers',
|
2096
|
+
'expects_continue',
|
2097
|
+
*[a for a in ParseHttpRequestResult.__slots__ if a != 'headers'],
|
2098
|
+
)
|
2099
|
+
|
2100
|
+
def __init__(
|
2101
|
+
self,
|
2102
|
+
*,
|
2103
|
+
method: str,
|
2104
|
+
path: str,
|
2105
|
+
headers: HttpHeaders,
|
2106
|
+
expects_continue: bool,
|
2107
|
+
|
2108
|
+
**kwargs: ta.Any,
|
2109
|
+
) -> None:
|
2110
|
+
super().__init__(
|
2111
|
+
headers=headers,
|
2112
|
+
**kwargs,
|
2113
|
+
)
|
2114
|
+
|
2115
|
+
self.method = method
|
2116
|
+
self.path = path
|
2117
|
+
self.expects_continue = expects_continue
|
2118
|
+
|
2119
|
+
headers: HttpHeaders
|
2120
|
+
|
2121
|
+
|
2122
|
+
#
|
2123
|
+
|
2124
|
+
|
2125
|
+
class HttpRequestParser:
|
2126
|
+
DEFAULT_SERVER_VERSION = HttpProtocolVersions.HTTP_1_0
|
2127
|
+
|
2128
|
+
# The default request version. This only affects responses up until the point where the request line is parsed, so
|
2129
|
+
# it mainly decides what the client gets back when sending a malformed request line.
|
2130
|
+
# Most web servers default to HTTP 0.9, i.e. don't send a status line.
|
2131
|
+
DEFAULT_REQUEST_VERSION = HttpProtocolVersions.HTTP_0_9
|
2132
|
+
|
2133
|
+
#
|
2134
|
+
|
2135
|
+
DEFAULT_MAX_LINE: int = 0x10000
|
2136
|
+
DEFAULT_MAX_HEADERS: int = 100
|
2137
|
+
|
2138
|
+
#
|
2139
|
+
|
2140
|
+
def __init__(
|
2141
|
+
self,
|
2142
|
+
*,
|
2143
|
+
server_version: HttpProtocolVersion = DEFAULT_SERVER_VERSION,
|
2144
|
+
|
2145
|
+
max_line: int = DEFAULT_MAX_LINE,
|
2146
|
+
max_headers: int = DEFAULT_MAX_HEADERS,
|
2147
|
+
) -> None:
|
2148
|
+
super().__init__()
|
2149
|
+
|
2150
|
+
if server_version >= HttpProtocolVersions.HTTP_2_0:
|
2151
|
+
raise ValueError(f'Unsupported protocol version: {server_version}')
|
2152
|
+
self._server_version = server_version
|
2153
|
+
|
2154
|
+
self._max_line = max_line
|
2155
|
+
self._max_headers = max_headers
|
2156
|
+
|
2157
|
+
#
|
2158
|
+
|
2159
|
+
@property
|
2160
|
+
def server_version(self) -> HttpProtocolVersion:
|
2161
|
+
return self._server_version
|
2162
|
+
|
2163
|
+
#
|
2164
|
+
|
2165
|
+
def _run_read_line_coro(
|
2166
|
+
self,
|
2167
|
+
gen: ta.Generator[int, bytes, T],
|
2168
|
+
read_line: ta.Callable[[int], bytes],
|
2169
|
+
) -> T:
|
2170
|
+
sz = next(gen)
|
2171
|
+
while True:
|
2172
|
+
try:
|
2173
|
+
sz = gen.send(read_line(sz))
|
2174
|
+
except StopIteration as e:
|
2175
|
+
return e.value
|
2176
|
+
|
2177
|
+
#
|
2178
|
+
|
2179
|
+
def parse_request_version(self, version_str: str) -> HttpProtocolVersion:
|
2180
|
+
if not version_str.startswith('HTTP/'):
|
2181
|
+
raise ValueError(version_str) # noqa
|
2182
|
+
|
2183
|
+
base_version_number = version_str.split('/', 1)[1]
|
2184
|
+
version_number_parts = base_version_number.split('.')
|
2185
|
+
|
2186
|
+
# RFC 2145 section 3.1 says there can be only one "." and
|
2187
|
+
# - major and minor numbers MUST be treated as separate integers;
|
2188
|
+
# - HTTP/2.4 is a lower version than HTTP/2.13, which in turn is lower than HTTP/12.3;
|
2189
|
+
# - Leading zeros MUST be ignored by recipients.
|
2190
|
+
if len(version_number_parts) != 2:
|
2191
|
+
raise ValueError(version_number_parts) # noqa
|
2192
|
+
if any(not component.isdigit() for component in version_number_parts):
|
2193
|
+
raise ValueError('non digit in http version') # noqa
|
2194
|
+
if any(len(component) > 10 for component in version_number_parts):
|
2195
|
+
raise ValueError('unreasonable length http version') # noqa
|
2196
|
+
|
2197
|
+
return HttpProtocolVersion(
|
2198
|
+
int(version_number_parts[0]),
|
2199
|
+
int(version_number_parts[1]),
|
2200
|
+
)
|
2201
|
+
|
2202
|
+
#
|
2203
|
+
|
2204
|
+
def coro_read_raw_headers(self) -> ta.Generator[int, bytes, ta.List[bytes]]:
|
2205
|
+
raw_headers: ta.List[bytes] = []
|
2206
|
+
while True:
|
2207
|
+
line = yield self._max_line + 1
|
2208
|
+
if len(line) > self._max_line:
|
2209
|
+
raise http.client.LineTooLong('header line')
|
2210
|
+
raw_headers.append(line)
|
2211
|
+
if len(raw_headers) > self._max_headers:
|
2212
|
+
raise http.client.HTTPException(f'got more than {self._max_headers} headers')
|
2213
|
+
if line in (b'\r\n', b'\n', b''):
|
2214
|
+
break
|
2215
|
+
return raw_headers
|
2216
|
+
|
2217
|
+
def read_raw_headers(self, read_line: ta.Callable[[int], bytes]) -> ta.List[bytes]:
|
2218
|
+
return self._run_read_line_coro(self.coro_read_raw_headers(), read_line)
|
2219
|
+
|
2220
|
+
def parse_raw_headers(self, raw_headers: ta.Sequence[bytes]) -> HttpHeaders:
|
2221
|
+
return http.client.parse_headers(io.BytesIO(b''.join(raw_headers)))
|
2222
|
+
|
2223
|
+
#
|
2224
|
+
|
2225
|
+
def coro_parse(self) -> ta.Generator[int, bytes, ParseHttpRequestResult]:
|
2226
|
+
raw_request_line = yield self._max_line + 1
|
2227
|
+
|
2228
|
+
# Common result kwargs
|
2229
|
+
|
2230
|
+
request_line = '-'
|
2231
|
+
request_version = self.DEFAULT_REQUEST_VERSION
|
2232
|
+
|
2233
|
+
# Set to min(server, request) when it gets that far, but if it fails before that the server authoritatively
|
2234
|
+
# responds with its own version.
|
2235
|
+
version = self._server_version
|
2236
|
+
|
2237
|
+
headers: HttpHeaders | None = None
|
2238
|
+
|
2239
|
+
close_connection = True
|
2240
|
+
|
2241
|
+
def result_kwargs():
|
2242
|
+
return dict(
|
2243
|
+
server_version=self._server_version,
|
2244
|
+
request_line=request_line,
|
2245
|
+
request_version=request_version,
|
2246
|
+
version=version,
|
2247
|
+
headers=headers,
|
2248
|
+
close_connection=close_connection,
|
2249
|
+
)
|
2250
|
+
|
2251
|
+
# Decode line
|
2252
|
+
|
2253
|
+
if len(raw_request_line) > self._max_line:
|
2254
|
+
return ParseHttpRequestError(
|
2255
|
+
code=http.HTTPStatus.REQUEST_URI_TOO_LONG,
|
2256
|
+
message='Request line too long',
|
2257
|
+
**result_kwargs(),
|
2258
|
+
)
|
2259
|
+
|
2260
|
+
if not raw_request_line:
|
2261
|
+
return EmptyParsedHttpResult(**result_kwargs())
|
2262
|
+
|
2263
|
+
request_line = raw_request_line.decode('iso-8859-1').rstrip('\r\n')
|
2264
|
+
|
2265
|
+
# Split words
|
2266
|
+
|
2267
|
+
words = request_line.split()
|
2268
|
+
if len(words) == 0:
|
2269
|
+
return EmptyParsedHttpResult(**result_kwargs())
|
2270
|
+
|
2271
|
+
# Parse and set version
|
2272
|
+
|
2273
|
+
if len(words) >= 3: # Enough to determine protocol version
|
2274
|
+
version_str = words[-1]
|
2275
|
+
try:
|
2276
|
+
request_version = self.parse_request_version(version_str)
|
2277
|
+
|
2278
|
+
except (ValueError, IndexError):
|
2279
|
+
return ParseHttpRequestError(
|
2280
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
2281
|
+
message=f'Bad request version ({version_str!r})',
|
2282
|
+
**result_kwargs(),
|
2283
|
+
)
|
2284
|
+
|
2285
|
+
if (
|
2286
|
+
request_version < HttpProtocolVersions.HTTP_0_9 or
|
2287
|
+
request_version >= HttpProtocolVersions.HTTP_2_0
|
2288
|
+
):
|
2289
|
+
return ParseHttpRequestError(
|
2290
|
+
code=http.HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
|
2291
|
+
message=f'Invalid HTTP version ({version_str})',
|
2292
|
+
**result_kwargs(),
|
2293
|
+
)
|
2294
|
+
|
2295
|
+
version = min([self._server_version, request_version])
|
2296
|
+
|
2297
|
+
if version >= HttpProtocolVersions.HTTP_1_1:
|
2298
|
+
close_connection = False
|
2299
|
+
|
2300
|
+
# Verify word count
|
2301
|
+
|
2302
|
+
if not 2 <= len(words) <= 3:
|
2303
|
+
return ParseHttpRequestError(
|
2304
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
2305
|
+
message=f'Bad request syntax ({request_line!r})',
|
2306
|
+
**result_kwargs(),
|
2307
|
+
)
|
2308
|
+
|
2309
|
+
# Parse method and path
|
2310
|
+
|
2311
|
+
method, path = words[:2]
|
2312
|
+
if len(words) == 2:
|
2313
|
+
close_connection = True
|
2314
|
+
if method != 'GET':
|
2315
|
+
return ParseHttpRequestError(
|
2316
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
2317
|
+
message=f'Bad HTTP/0.9 request type ({method!r})',
|
2318
|
+
**result_kwargs(),
|
2319
|
+
)
|
2320
|
+
|
2321
|
+
# gh-87389: The purpose of replacing '//' with '/' is to protect against open redirect attacks possibly
|
2322
|
+
# triggered if the path starts with '//' because http clients treat //path as an absolute URI without scheme
|
2323
|
+
# (similar to http://path) rather than a path.
|
2324
|
+
if path.startswith('//'):
|
2325
|
+
path = '/' + path.lstrip('/') # Reduce to a single /
|
2326
|
+
|
2327
|
+
# Parse headers
|
2328
|
+
|
2329
|
+
try:
|
2330
|
+
raw_gen = self.coro_read_raw_headers()
|
2331
|
+
raw_sz = next(raw_gen)
|
2332
|
+
while True:
|
2333
|
+
buf = yield raw_sz
|
2334
|
+
try:
|
2335
|
+
raw_sz = raw_gen.send(buf)
|
2336
|
+
except StopIteration as e:
|
2337
|
+
raw_headers = e.value
|
2338
|
+
break
|
2339
|
+
|
2340
|
+
headers = self.parse_raw_headers(raw_headers)
|
2341
|
+
|
2342
|
+
except http.client.LineTooLong as err:
|
2343
|
+
return ParseHttpRequestError(
|
2344
|
+
code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
|
2345
|
+
message=('Line too long', str(err)),
|
2346
|
+
**result_kwargs(),
|
2347
|
+
)
|
2348
|
+
|
2349
|
+
except http.client.HTTPException as err:
|
2350
|
+
return ParseHttpRequestError(
|
2351
|
+
code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
|
2352
|
+
message=('Too many headers', str(err)),
|
2353
|
+
**result_kwargs(),
|
2354
|
+
)
|
2355
|
+
|
2356
|
+
# Check for connection directive
|
2357
|
+
|
2358
|
+
conn_type = headers.get('Connection', '')
|
2359
|
+
if conn_type.lower() == 'close':
|
2360
|
+
close_connection = True
|
2361
|
+
elif (
|
2362
|
+
conn_type.lower() == 'keep-alive' and
|
2363
|
+
version >= HttpProtocolVersions.HTTP_1_1
|
2364
|
+
):
|
2365
|
+
close_connection = False
|
2366
|
+
|
2367
|
+
# Check for expect directive
|
2368
|
+
|
2369
|
+
expect = headers.get('Expect', '')
|
2370
|
+
if (
|
2371
|
+
expect.lower() == '100-continue' and
|
2372
|
+
version >= HttpProtocolVersions.HTTP_1_1
|
2373
|
+
):
|
2374
|
+
expects_continue = True
|
2375
|
+
else:
|
2376
|
+
expects_continue = False
|
2377
|
+
|
2378
|
+
# Return
|
2379
|
+
|
2380
|
+
return ParsedHttpRequest(
|
2381
|
+
method=method,
|
2382
|
+
path=path,
|
2383
|
+
expects_continue=expects_continue,
|
2384
|
+
**result_kwargs(),
|
2385
|
+
)
|
2386
|
+
|
2387
|
+
def parse(self, read_line: ta.Callable[[int], bytes]) -> ParseHttpRequestResult:
|
2388
|
+
return self._run_read_line_coro(self.coro_parse(), read_line)
|
2389
|
+
|
2390
|
+
|
1913
2391
|
########################################
|
1914
2392
|
# ../../../omlish/lite/inject.py
|
1915
2393
|
|
@@ -1919,12 +2397,22 @@ def timeslice(period: int, when: float) -> int:
|
|
1919
2397
|
|
1920
2398
|
|
1921
2399
|
@dc.dataclass(frozen=True)
|
1922
|
-
class InjectorKey:
|
2400
|
+
class InjectorKey(ta.Generic[T]):
|
1923
2401
|
cls: InjectorKeyCls
|
1924
2402
|
tag: ta.Any = None
|
1925
2403
|
array: bool = False
|
1926
2404
|
|
1927
2405
|
|
2406
|
+
def is_valid_injector_key_cls(cls: ta.Any) -> bool:
|
2407
|
+
return isinstance(cls, type) or is_new_type(cls)
|
2408
|
+
|
2409
|
+
|
2410
|
+
def check_valid_injector_key_cls(cls: T) -> T:
|
2411
|
+
if not is_valid_injector_key_cls(cls):
|
2412
|
+
raise TypeError(cls)
|
2413
|
+
return cls
|
2414
|
+
|
2415
|
+
|
1928
2416
|
##
|
1929
2417
|
|
1930
2418
|
|
@@ -1968,6 +2456,12 @@ class Injector(abc.ABC):
|
|
1968
2456
|
def inject(self, obj: ta.Any) -> ta.Any:
|
1969
2457
|
raise NotImplementedError
|
1970
2458
|
|
2459
|
+
def __getitem__(
|
2460
|
+
self,
|
2461
|
+
target: ta.Union[InjectorKey[T], ta.Type[T]],
|
2462
|
+
) -> T:
|
2463
|
+
return self.provide(target)
|
2464
|
+
|
1971
2465
|
|
1972
2466
|
###
|
1973
2467
|
# exceptions
|
@@ -2000,7 +2494,7 @@ def as_injector_key(o: ta.Any) -> InjectorKey:
|
|
2000
2494
|
raise TypeError(o)
|
2001
2495
|
if isinstance(o, InjectorKey):
|
2002
2496
|
return o
|
2003
|
-
if
|
2497
|
+
if is_valid_injector_key_cls(o):
|
2004
2498
|
return InjectorKey(o)
|
2005
2499
|
raise TypeError(o)
|
2006
2500
|
|
@@ -2332,8 +2826,8 @@ class InjectorBinder:
|
|
2332
2826
|
to_fn = obj
|
2333
2827
|
if key is None:
|
2334
2828
|
sig = _injection_signature(obj)
|
2335
|
-
|
2336
|
-
key = InjectorKey(
|
2829
|
+
key_cls = check_valid_injector_key_cls(sig.return_annotation)
|
2830
|
+
key = InjectorKey(key_cls)
|
2337
2831
|
else:
|
2338
2832
|
if to_const is not None:
|
2339
2833
|
raise TypeError('Cannot bind instance with to_const')
|
@@ -2387,7 +2881,7 @@ class InjectorBinder:
|
|
2387
2881
|
# injector
|
2388
2882
|
|
2389
2883
|
|
2390
|
-
_INJECTOR_INJECTOR_KEY = InjectorKey(Injector)
|
2884
|
+
_INJECTOR_INJECTOR_KEY: InjectorKey[Injector] = InjectorKey(Injector)
|
2391
2885
|
|
2392
2886
|
|
2393
2887
|
class _Injector(Injector):
|
@@ -2542,7 +3036,6 @@ sd_iovec._fields_ = [
|
|
2542
3036
|
def sd_libsystemd() -> ta.Any:
|
2543
3037
|
lib = ct.CDLL('libsystemd.so.0')
|
2544
3038
|
|
2545
|
-
lib.sd_journal_sendv = lib['sd_journal_sendv'] # type: ignore
|
2546
3039
|
lib.sd_journal_sendv.restype = ct.c_int
|
2547
3040
|
lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
|
2548
3041
|
|
@@ -3582,6 +4075,36 @@ def get_poller_impl() -> ta.Type[Poller]:
|
|
3582
4075
|
return SelectPoller
|
3583
4076
|
|
3584
4077
|
|
4078
|
+
########################################
|
4079
|
+
# ../../../omlish/lite/http/handlers.py
|
4080
|
+
|
4081
|
+
|
4082
|
+
@dc.dataclass(frozen=True)
|
4083
|
+
class HttpHandlerRequest:
|
4084
|
+
client_address: SocketAddress
|
4085
|
+
method: str
|
4086
|
+
path: str
|
4087
|
+
headers: HttpHeaders
|
4088
|
+
data: ta.Optional[bytes]
|
4089
|
+
|
4090
|
+
|
4091
|
+
@dc.dataclass(frozen=True)
|
4092
|
+
class HttpHandlerResponse:
|
4093
|
+
status: ta.Union[http.HTTPStatus, int]
|
4094
|
+
|
4095
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None
|
4096
|
+
data: ta.Optional[bytes] = None
|
4097
|
+
close_connection: ta.Optional[bool] = None
|
4098
|
+
|
4099
|
+
|
4100
|
+
class HttpHandlerError(Exception):
|
4101
|
+
pass
|
4102
|
+
|
4103
|
+
|
4104
|
+
class UnsupportedMethodHttpHandlerError(Exception):
|
4105
|
+
pass
|
4106
|
+
|
4107
|
+
|
3585
4108
|
########################################
|
3586
4109
|
# ../configs.py
|
3587
4110
|
|
@@ -3703,6 +4226,566 @@ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.An
|
|
3703
4226
|
return out
|
3704
4227
|
|
3705
4228
|
|
4229
|
+
########################################
|
4230
|
+
# ../../../omlish/lite/http/coroserver.py
|
4231
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
4232
|
+
# --------------------------------------------
|
4233
|
+
#
|
4234
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
4235
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
4236
|
+
# documentation.
|
4237
|
+
#
|
4238
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
4239
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
4240
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
4241
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
4242
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
|
4243
|
+
# alone or in any derivative version prepared by Licensee.
|
4244
|
+
#
|
4245
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
4246
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
4247
|
+
# any such work a brief summary of the changes made to Python.
|
4248
|
+
#
|
4249
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
4250
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
4251
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
4252
|
+
# RIGHTS.
|
4253
|
+
#
|
4254
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
4255
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
4256
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
4257
|
+
#
|
4258
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
4259
|
+
#
|
4260
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
4261
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
4262
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
4263
|
+
#
|
4264
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
4265
|
+
# License Agreement.
|
4266
|
+
"""
|
4267
|
+
"Test suite" lol:
|
4268
|
+
|
4269
|
+
curl -v localhost:8000
|
4270
|
+
curl -v localhost:8000 -d 'foo'
|
4271
|
+
curl -v -XFOO localhost:8000 -d 'foo'
|
4272
|
+
curl -v -XPOST -H 'Expect: 100-Continue' localhost:8000 -d 'foo'
|
4273
|
+
|
4274
|
+
curl -v -0 localhost:8000
|
4275
|
+
curl -v -0 localhost:8000 -d 'foo'
|
4276
|
+
curl -v -0 -XFOO localhost:8000 -d 'foo'
|
4277
|
+
|
4278
|
+
curl -v -XPOST localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
|
4279
|
+
curl -v -XPOST localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
|
4280
|
+
curl -v -XFOO localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
|
4281
|
+
curl -v -XFOO localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
|
4282
|
+
"""
|
4283
|
+
|
4284
|
+
|
4285
|
+
##
|
4286
|
+
|
4287
|
+
|
4288
|
+
class CoroHttpServer:
|
4289
|
+
"""
|
4290
|
+
Adapted from stdlib:
|
4291
|
+
- https://github.com/python/cpython/blob/4b4e0dbdf49adc91c35a357ad332ab3abd4c31b1/Lib/http/server.py#L146
|
4292
|
+
"""
|
4293
|
+
|
4294
|
+
#
|
4295
|
+
|
4296
|
+
def __init__(
|
4297
|
+
self,
|
4298
|
+
client_address: SocketAddress,
|
4299
|
+
*,
|
4300
|
+
handler: HttpHandler,
|
4301
|
+
parser: HttpRequestParser = HttpRequestParser(),
|
4302
|
+
|
4303
|
+
default_content_type: ta.Optional[str] = None,
|
4304
|
+
|
4305
|
+
error_message_format: ta.Optional[str] = None,
|
4306
|
+
error_content_type: ta.Optional[str] = None,
|
4307
|
+
) -> None:
|
4308
|
+
super().__init__()
|
4309
|
+
|
4310
|
+
self._client_address = client_address
|
4311
|
+
|
4312
|
+
self._handler = handler
|
4313
|
+
self._parser = parser
|
4314
|
+
|
4315
|
+
self._default_content_type = default_content_type or self.DEFAULT_CONTENT_TYPE
|
4316
|
+
|
4317
|
+
self._error_message_format = error_message_format or self.DEFAULT_ERROR_MESSAGE
|
4318
|
+
self._error_content_type = error_content_type or self.DEFAULT_ERROR_CONTENT_TYPE
|
4319
|
+
|
4320
|
+
#
|
4321
|
+
|
4322
|
+
@property
|
4323
|
+
def client_address(self) -> SocketAddress:
|
4324
|
+
return self._client_address
|
4325
|
+
|
4326
|
+
@property
|
4327
|
+
def handler(self) -> HttpHandler:
|
4328
|
+
return self._handler
|
4329
|
+
|
4330
|
+
@property
|
4331
|
+
def parser(self) -> HttpRequestParser:
|
4332
|
+
return self._parser
|
4333
|
+
|
4334
|
+
#
|
4335
|
+
|
4336
|
+
def _format_timestamp(self, timestamp: ta.Optional[float] = None) -> str:
|
4337
|
+
if timestamp is None:
|
4338
|
+
timestamp = time.time()
|
4339
|
+
return email.utils.formatdate(timestamp, usegmt=True)
|
4340
|
+
|
4341
|
+
#
|
4342
|
+
|
4343
|
+
def _header_encode(self, s: str) -> bytes:
|
4344
|
+
return s.encode('latin-1', 'strict')
|
4345
|
+
|
4346
|
+
class _Header(ta.NamedTuple):
|
4347
|
+
key: str
|
4348
|
+
value: str
|
4349
|
+
|
4350
|
+
def _format_header_line(self, h: _Header) -> str:
|
4351
|
+
return f'{h.key}: {h.value}\r\n'
|
4352
|
+
|
4353
|
+
def _get_header_close_connection_action(self, h: _Header) -> ta.Optional[bool]:
|
4354
|
+
if h.key.lower() != 'connection':
|
4355
|
+
return None
|
4356
|
+
elif h.value.lower() == 'close':
|
4357
|
+
return True
|
4358
|
+
elif h.value.lower() == 'keep-alive':
|
4359
|
+
return False
|
4360
|
+
else:
|
4361
|
+
return None
|
4362
|
+
|
4363
|
+
def _make_default_headers(self) -> ta.List[_Header]:
|
4364
|
+
return [
|
4365
|
+
self._Header('Date', self._format_timestamp()),
|
4366
|
+
]
|
4367
|
+
|
4368
|
+
#
|
4369
|
+
|
4370
|
+
_STATUS_RESPONSES: ta.Mapping[int, ta.Tuple[str, str]] = {
|
4371
|
+
v: (v.phrase, v.description)
|
4372
|
+
for v in http.HTTPStatus.__members__.values()
|
4373
|
+
}
|
4374
|
+
|
4375
|
+
def _format_status_line(
|
4376
|
+
self,
|
4377
|
+
version: HttpProtocolVersion,
|
4378
|
+
code: ta.Union[http.HTTPStatus, int],
|
4379
|
+
message: ta.Optional[str] = None,
|
4380
|
+
) -> str:
|
4381
|
+
if message is None:
|
4382
|
+
if code in self._STATUS_RESPONSES:
|
4383
|
+
message = self._STATUS_RESPONSES[code][0]
|
4384
|
+
else:
|
4385
|
+
message = ''
|
4386
|
+
|
4387
|
+
return f'{version} {int(code)} {message}\r\n'
|
4388
|
+
|
4389
|
+
#
|
4390
|
+
|
4391
|
+
@dc.dataclass(frozen=True)
|
4392
|
+
class _Response:
|
4393
|
+
version: HttpProtocolVersion
|
4394
|
+
code: http.HTTPStatus
|
4395
|
+
|
4396
|
+
message: ta.Optional[str] = None
|
4397
|
+
headers: ta.Optional[ta.Sequence['CoroHttpServer._Header']] = None
|
4398
|
+
data: ta.Optional[bytes] = None
|
4399
|
+
close_connection: ta.Optional[bool] = False
|
4400
|
+
|
4401
|
+
def get_header(self, key: str) -> ta.Optional['CoroHttpServer._Header']:
|
4402
|
+
for h in self.headers or []:
|
4403
|
+
if h.key.lower() == key.lower():
|
4404
|
+
return h
|
4405
|
+
return None
|
4406
|
+
|
4407
|
+
#
|
4408
|
+
|
4409
|
+
def _build_response_bytes(self, a: _Response) -> bytes:
|
4410
|
+
out = io.BytesIO()
|
4411
|
+
|
4412
|
+
if a.version >= HttpProtocolVersions.HTTP_1_0:
|
4413
|
+
out.write(self._header_encode(self._format_status_line(
|
4414
|
+
a.version,
|
4415
|
+
a.code,
|
4416
|
+
a.message,
|
4417
|
+
)))
|
4418
|
+
|
4419
|
+
for h in a.headers or []:
|
4420
|
+
out.write(self._header_encode(self._format_header_line(h)))
|
4421
|
+
|
4422
|
+
out.write(b'\r\n')
|
4423
|
+
|
4424
|
+
if a.data is not None:
|
4425
|
+
out.write(a.data)
|
4426
|
+
|
4427
|
+
return out.getvalue()
|
4428
|
+
|
4429
|
+
#
|
4430
|
+
|
4431
|
+
DEFAULT_CONTENT_TYPE = 'text/plain'
|
4432
|
+
|
4433
|
+
def _preprocess_response(self, resp: _Response) -> _Response:
|
4434
|
+
nh: ta.List[CoroHttpServer._Header] = []
|
4435
|
+
kw: ta.Dict[str, ta.Any] = {}
|
4436
|
+
|
4437
|
+
if resp.get_header('Content-Type') is None:
|
4438
|
+
nh.append(self._Header('Content-Type', self._default_content_type))
|
4439
|
+
if resp.data is not None and resp.get_header('Content-Length') is None:
|
4440
|
+
nh.append(self._Header('Content-Length', str(len(resp.data))))
|
4441
|
+
|
4442
|
+
if nh:
|
4443
|
+
kw.update(headers=[*(resp.headers or []), *nh])
|
4444
|
+
|
4445
|
+
if (clh := resp.get_header('Connection')) is not None:
|
4446
|
+
if self._get_header_close_connection_action(clh):
|
4447
|
+
kw.update(close_connection=True)
|
4448
|
+
|
4449
|
+
if not kw:
|
4450
|
+
return resp
|
4451
|
+
return dc.replace(resp, **kw)
|
4452
|
+
|
4453
|
+
#
|
4454
|
+
|
4455
|
+
@dc.dataclass(frozen=True)
|
4456
|
+
class Error:
|
4457
|
+
version: HttpProtocolVersion
|
4458
|
+
code: http.HTTPStatus
|
4459
|
+
message: str
|
4460
|
+
explain: str
|
4461
|
+
|
4462
|
+
method: ta.Optional[str] = None
|
4463
|
+
|
4464
|
+
def _build_error(
|
4465
|
+
self,
|
4466
|
+
code: ta.Union[http.HTTPStatus, int],
|
4467
|
+
message: ta.Optional[str] = None,
|
4468
|
+
explain: ta.Optional[str] = None,
|
4469
|
+
*,
|
4470
|
+
version: ta.Optional[HttpProtocolVersion] = None,
|
4471
|
+
method: ta.Optional[str] = None,
|
4472
|
+
) -> Error:
|
4473
|
+
code = http.HTTPStatus(code)
|
4474
|
+
|
4475
|
+
try:
|
4476
|
+
short_msg, long_msg = self._STATUS_RESPONSES[code]
|
4477
|
+
except KeyError:
|
4478
|
+
short_msg, long_msg = '???', '???'
|
4479
|
+
if message is None:
|
4480
|
+
message = short_msg
|
4481
|
+
if explain is None:
|
4482
|
+
explain = long_msg
|
4483
|
+
|
4484
|
+
if version is None:
|
4485
|
+
version = self._parser.server_version
|
4486
|
+
|
4487
|
+
return self.Error(
|
4488
|
+
version=version,
|
4489
|
+
code=code,
|
4490
|
+
message=message,
|
4491
|
+
explain=explain,
|
4492
|
+
|
4493
|
+
method=method,
|
4494
|
+
)
|
4495
|
+
|
4496
|
+
#
|
4497
|
+
|
4498
|
+
DEFAULT_ERROR_MESSAGE = textwrap.dedent("""\
|
4499
|
+
<!DOCTYPE HTML>
|
4500
|
+
<html lang="en">
|
4501
|
+
<head>
|
4502
|
+
<meta charset="utf-8">
|
4503
|
+
<title>Error response</title>
|
4504
|
+
</head>
|
4505
|
+
<body>
|
4506
|
+
<h1>Error response</h1>
|
4507
|
+
<p>Error code: %(code)d</p>
|
4508
|
+
<p>Message: %(message)s.</p>
|
4509
|
+
<p>Error code explanation: %(code)s - %(explain)s.</p>
|
4510
|
+
</body>
|
4511
|
+
</html>
|
4512
|
+
""")
|
4513
|
+
|
4514
|
+
DEFAULT_ERROR_CONTENT_TYPE = 'text/html;charset=utf-8'
|
4515
|
+
|
4516
|
+
def _build_error_response(self, err: Error) -> _Response:
|
4517
|
+
headers: ta.List[CoroHttpServer._Header] = [
|
4518
|
+
*self._make_default_headers(),
|
4519
|
+
self._Header('Connection', 'close'),
|
4520
|
+
]
|
4521
|
+
|
4522
|
+
# Message body is omitted for cases described in:
|
4523
|
+
# - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
|
4524
|
+
# - RFC7231: 6.3.6. 205(Reset Content)
|
4525
|
+
data: ta.Optional[bytes] = None
|
4526
|
+
if (
|
4527
|
+
err.code >= http.HTTPStatus.OK and
|
4528
|
+
err.code not in (
|
4529
|
+
http.HTTPStatus.NO_CONTENT,
|
4530
|
+
http.HTTPStatus.RESET_CONTENT,
|
4531
|
+
http.HTTPStatus.NOT_MODIFIED,
|
4532
|
+
)
|
4533
|
+
):
|
4534
|
+
# HTML encode to prevent Cross Site Scripting attacks (see bug #1100201)
|
4535
|
+
content = self._error_message_format.format(
|
4536
|
+
code=err.code,
|
4537
|
+
message=html.escape(err.message, quote=False),
|
4538
|
+
explain=html.escape(err.explain, quote=False),
|
4539
|
+
)
|
4540
|
+
body = content.encode('UTF-8', 'replace')
|
4541
|
+
|
4542
|
+
headers.extend([
|
4543
|
+
self._Header('Content-Type', self._error_content_type),
|
4544
|
+
self._Header('Content-Length', str(len(body))),
|
4545
|
+
])
|
4546
|
+
|
4547
|
+
if err.method != 'HEAD' and body:
|
4548
|
+
data = body
|
4549
|
+
|
4550
|
+
return self._Response(
|
4551
|
+
version=err.version,
|
4552
|
+
code=err.code,
|
4553
|
+
message=err.message,
|
4554
|
+
headers=headers,
|
4555
|
+
data=data,
|
4556
|
+
close_connection=True,
|
4557
|
+
)
|
4558
|
+
|
4559
|
+
#
|
4560
|
+
|
4561
|
+
class Io(abc.ABC): # noqa
|
4562
|
+
pass
|
4563
|
+
|
4564
|
+
#
|
4565
|
+
|
4566
|
+
class AnyLogIo(Io):
|
4567
|
+
pass
|
4568
|
+
|
4569
|
+
@dc.dataclass(frozen=True)
|
4570
|
+
class ParsedRequestLogIo(AnyLogIo):
|
4571
|
+
request: ParsedHttpRequest
|
4572
|
+
|
4573
|
+
@dc.dataclass(frozen=True)
|
4574
|
+
class ErrorLogIo(AnyLogIo):
|
4575
|
+
error: 'CoroHttpServer.Error'
|
4576
|
+
|
4577
|
+
#
|
4578
|
+
|
4579
|
+
class AnyReadIo(Io): # noqa
|
4580
|
+
pass
|
4581
|
+
|
4582
|
+
@dc.dataclass(frozen=True)
|
4583
|
+
class ReadIo(AnyReadIo):
|
4584
|
+
sz: int
|
4585
|
+
|
4586
|
+
@dc.dataclass(frozen=True)
|
4587
|
+
class ReadLineIo(AnyReadIo):
|
4588
|
+
sz: int
|
4589
|
+
|
4590
|
+
#
|
4591
|
+
|
4592
|
+
@dc.dataclass(frozen=True)
|
4593
|
+
class WriteIo(Io):
|
4594
|
+
data: bytes
|
4595
|
+
|
4596
|
+
#
|
4597
|
+
|
4598
|
+
def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
|
4599
|
+
while True:
|
4600
|
+
gen = self.coro_handle_one()
|
4601
|
+
|
4602
|
+
o = next(gen)
|
4603
|
+
i: ta.Optional[bytes]
|
4604
|
+
while True:
|
4605
|
+
if isinstance(o, self.AnyLogIo):
|
4606
|
+
i = None
|
4607
|
+
yield o
|
4608
|
+
|
4609
|
+
elif isinstance(o, self.AnyReadIo):
|
4610
|
+
i = check_isinstance((yield o), bytes)
|
4611
|
+
|
4612
|
+
elif isinstance(o, self._Response):
|
4613
|
+
i = None
|
4614
|
+
r = self._preprocess_response(o)
|
4615
|
+
b = self._build_response_bytes(r)
|
4616
|
+
check_none((yield self.WriteIo(b)))
|
4617
|
+
|
4618
|
+
else:
|
4619
|
+
raise TypeError(o)
|
4620
|
+
|
4621
|
+
try:
|
4622
|
+
o = gen.send(i)
|
4623
|
+
except EOFError:
|
4624
|
+
return
|
4625
|
+
except StopIteration:
|
4626
|
+
break
|
4627
|
+
|
4628
|
+
def coro_handle_one(self) -> ta.Generator[
|
4629
|
+
ta.Union[AnyLogIo, AnyReadIo, _Response],
|
4630
|
+
ta.Optional[bytes],
|
4631
|
+
None,
|
4632
|
+
]:
|
4633
|
+
# Parse request
|
4634
|
+
|
4635
|
+
gen = self._parser.coro_parse()
|
4636
|
+
sz = next(gen)
|
4637
|
+
while True:
|
4638
|
+
try:
|
4639
|
+
line = check_isinstance((yield self.ReadLineIo(sz)), bytes)
|
4640
|
+
sz = gen.send(line)
|
4641
|
+
except StopIteration as e:
|
4642
|
+
parsed = e.value
|
4643
|
+
break
|
4644
|
+
|
4645
|
+
if isinstance(parsed, EmptyParsedHttpResult):
|
4646
|
+
raise EOFError # noqa
|
4647
|
+
|
4648
|
+
if isinstance(parsed, ParseHttpRequestError):
|
4649
|
+
err = self._build_error(
|
4650
|
+
parsed.code,
|
4651
|
+
*parsed.message,
|
4652
|
+
version=parsed.version,
|
4653
|
+
)
|
4654
|
+
yield self.ErrorLogIo(err)
|
4655
|
+
yield self._build_error_response(err)
|
4656
|
+
return
|
4657
|
+
|
4658
|
+
parsed = check_isinstance(parsed, ParsedHttpRequest)
|
4659
|
+
|
4660
|
+
# Log
|
4661
|
+
|
4662
|
+
check_none((yield self.ParsedRequestLogIo(parsed)))
|
4663
|
+
|
4664
|
+
# Handle CONTINUE
|
4665
|
+
|
4666
|
+
if parsed.expects_continue:
|
4667
|
+
# https://bugs.python.org/issue1491
|
4668
|
+
# https://github.com/python/cpython/commit/0f476d49f8d4aa84210392bf13b59afc67b32b31
|
4669
|
+
yield self._Response(
|
4670
|
+
version=parsed.version,
|
4671
|
+
code=http.HTTPStatus.CONTINUE,
|
4672
|
+
)
|
4673
|
+
|
4674
|
+
# Read data
|
4675
|
+
|
4676
|
+
request_data: ta.Optional[bytes]
|
4677
|
+
if (cl := parsed.headers.get('Content-Length')) is not None:
|
4678
|
+
request_data = check_isinstance((yield self.ReadIo(int(cl))), bytes)
|
4679
|
+
else:
|
4680
|
+
request_data = None
|
4681
|
+
|
4682
|
+
# Build request
|
4683
|
+
|
4684
|
+
handler_request = HttpHandlerRequest(
|
4685
|
+
client_address=self._client_address,
|
4686
|
+
method=check_not_none(parsed.method),
|
4687
|
+
path=parsed.path,
|
4688
|
+
headers=parsed.headers,
|
4689
|
+
data=request_data,
|
4690
|
+
)
|
4691
|
+
|
4692
|
+
# Build handler response
|
4693
|
+
|
4694
|
+
try:
|
4695
|
+
handler_response = self._handler(handler_request)
|
4696
|
+
|
4697
|
+
except UnsupportedMethodHttpHandlerError:
|
4698
|
+
err = self._build_error(
|
4699
|
+
http.HTTPStatus.NOT_IMPLEMENTED,
|
4700
|
+
f'Unsupported method ({parsed.method!r})',
|
4701
|
+
version=parsed.version,
|
4702
|
+
method=parsed.method,
|
4703
|
+
)
|
4704
|
+
yield self.ErrorLogIo(err)
|
4705
|
+
yield self._build_error_response(err)
|
4706
|
+
return
|
4707
|
+
|
4708
|
+
# Build internal response
|
4709
|
+
|
4710
|
+
response_headers = handler_response.headers or {}
|
4711
|
+
response_data = handler_response.data
|
4712
|
+
|
4713
|
+
headers: ta.List[CoroHttpServer._Header] = [
|
4714
|
+
*self._make_default_headers(),
|
4715
|
+
]
|
4716
|
+
|
4717
|
+
for k, v in response_headers.items():
|
4718
|
+
headers.append(self._Header(k, v))
|
4719
|
+
|
4720
|
+
if handler_response.close_connection and 'Connection' not in headers:
|
4721
|
+
headers.append(self._Header('Connection', 'close'))
|
4722
|
+
|
4723
|
+
yield self._Response(
|
4724
|
+
version=parsed.version,
|
4725
|
+
code=http.HTTPStatus(handler_response.status),
|
4726
|
+
headers=headers,
|
4727
|
+
data=response_data,
|
4728
|
+
close_connection=handler_response.close_connection,
|
4729
|
+
)
|
4730
|
+
|
4731
|
+
|
4732
|
+
##
|
4733
|
+
|
4734
|
+
|
4735
|
+
class CoroHttpServerSocketHandler(SocketHandler):
|
4736
|
+
def __init__(
|
4737
|
+
self,
|
4738
|
+
client_address: SocketAddress,
|
4739
|
+
rfile: ta.BinaryIO,
|
4740
|
+
wfile: ta.BinaryIO,
|
4741
|
+
*,
|
4742
|
+
server_factory: CoroHttpServerFactory,
|
4743
|
+
log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
|
4744
|
+
) -> None:
|
4745
|
+
super().__init__(
|
4746
|
+
client_address,
|
4747
|
+
rfile,
|
4748
|
+
wfile,
|
4749
|
+
)
|
4750
|
+
|
4751
|
+
self._server_factory = server_factory
|
4752
|
+
self._log_handler = log_handler
|
4753
|
+
|
4754
|
+
def handle(self) -> None:
|
4755
|
+
server = self._server_factory(self._client_address)
|
4756
|
+
|
4757
|
+
gen = server.coro_handle()
|
4758
|
+
|
4759
|
+
o = next(gen)
|
4760
|
+
while True:
|
4761
|
+
if isinstance(o, CoroHttpServer.AnyLogIo):
|
4762
|
+
i = None
|
4763
|
+
if self._log_handler is not None:
|
4764
|
+
self._log_handler(server, o)
|
4765
|
+
|
4766
|
+
elif isinstance(o, CoroHttpServer.ReadIo):
|
4767
|
+
i = self._rfile.read(o.sz)
|
4768
|
+
|
4769
|
+
elif isinstance(o, CoroHttpServer.ReadLineIo):
|
4770
|
+
i = self._rfile.readline(o.sz)
|
4771
|
+
|
4772
|
+
elif isinstance(o, CoroHttpServer.WriteIo):
|
4773
|
+
i = None
|
4774
|
+
self._wfile.write(o.data)
|
4775
|
+
self._wfile.flush()
|
4776
|
+
|
4777
|
+
else:
|
4778
|
+
raise TypeError(o)
|
4779
|
+
|
4780
|
+
try:
|
4781
|
+
if i is not None:
|
4782
|
+
o = gen.send(i)
|
4783
|
+
else:
|
4784
|
+
o = next(gen)
|
4785
|
+
except StopIteration:
|
4786
|
+
break
|
4787
|
+
|
4788
|
+
|
3706
4789
|
########################################
|
3707
4790
|
# ../types.py
|
3708
4791
|
|
@@ -5740,13 +6823,13 @@ class Supervisor:
|
|
5740
6823
|
|
5741
6824
|
|
5742
6825
|
########################################
|
5743
|
-
#
|
6826
|
+
# ../inject.py
|
5744
6827
|
|
5745
6828
|
|
5746
6829
|
##
|
5747
6830
|
|
5748
6831
|
|
5749
|
-
def
|
6832
|
+
def bind_server(
|
5750
6833
|
config: ServerConfig,
|
5751
6834
|
*,
|
5752
6835
|
server_epoch: ta.Optional[ServerEpoch] = None,
|
@@ -5795,6 +6878,10 @@ def build_server_bindings(
|
|
5795
6878
|
return inj.as_bindings(*lst)
|
5796
6879
|
|
5797
6880
|
|
6881
|
+
########################################
|
6882
|
+
# main.py
|
6883
|
+
|
6884
|
+
|
5798
6885
|
##
|
5799
6886
|
|
5800
6887
|
|
@@ -5803,6 +6890,10 @@ def main(
|
|
5803
6890
|
*,
|
5804
6891
|
no_logging: bool = False,
|
5805
6892
|
) -> None:
|
6893
|
+
server_cls = CoroHttpServer # noqa
|
6894
|
+
|
6895
|
+
#
|
6896
|
+
|
5806
6897
|
import argparse
|
5807
6898
|
|
5808
6899
|
parser = argparse.ArgumentParser()
|
@@ -5836,14 +6927,14 @@ def main(
|
|
5836
6927
|
prepare=prepare_server_config,
|
5837
6928
|
)
|
5838
6929
|
|
5839
|
-
injector = inj.create_injector(
|
6930
|
+
injector = inj.create_injector(bind_server(
|
5840
6931
|
config,
|
5841
6932
|
server_epoch=ServerEpoch(epoch),
|
5842
6933
|
inherited_fds=inherited_fds,
|
5843
6934
|
))
|
5844
6935
|
|
5845
|
-
context = injector
|
5846
|
-
supervisor = injector
|
6936
|
+
context = injector[ServerContext]
|
6937
|
+
supervisor = injector[Supervisor]
|
5847
6938
|
|
5848
6939
|
try:
|
5849
6940
|
supervisor.main()
|