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.
@@ -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 isinstance(o, type) or is_new_type(o):
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
- ty = check_isinstance(sig.return_annotation, type)
2336
- key = InjectorKey(ty)
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
- # main.py
6826
+ # ../inject.py
5744
6827
 
5745
6828
 
5746
6829
  ##
5747
6830
 
5748
6831
 
5749
- def build_server_bindings(
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(build_server_bindings(
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.provide(ServerContext)
5846
- supervisor = injector.provide(Supervisor)
6936
+ context = injector[ServerContext]
6937
+ supervisor = injector[Supervisor]
5847
6938
 
5848
6939
  try:
5849
6940
  supervisor.main()