ominfra 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev124__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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()
|