omlish 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev125__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.
- omlish/__about__.py +3 -2
- omlish/__init__.py +3 -0
- omlish/antlr/_runtime/LICENSE.txt +28 -0
- omlish/c3.py +35 -37
- omlish/dataclasses/__init__.py +4 -0
- omlish/dataclasses/utils.py +22 -4
- omlish/http/consts.py +2 -0
- omlish/inject/impl/inspect.py +4 -2
- omlish/lang/__init__.py +3 -0
- omlish/lang/typing.py +8 -0
- omlish/lite/check.py +5 -0
- omlish/lite/http/__init__.py +0 -0
- omlish/lite/http/coroserver.py +585 -0
- omlish/lite/http/handlers.py +36 -0
- omlish/lite/http/parsing.py +376 -0
- omlish/lite/http/versions.py +17 -0
- omlish/lite/inject.py +153 -82
- omlish/lite/journald.py +0 -1
- omlish/lite/runtime.py +1 -2
- omlish/lite/socket.py +77 -0
- omlish/lite/socketserver.py +66 -0
- omlish/lite/typing.py +13 -0
- omlish-0.0.0.dev125.dist-info/METADATA +100 -0
- {omlish-0.0.0.dev123.dist-info → omlish-0.0.0.dev125.dist-info}/RECORD +28 -19
- {omlish-0.0.0.dev123.dist-info → omlish-0.0.0.dev125.dist-info}/WHEEL +1 -1
- omlish-0.0.0.dev123.dist-info/METADATA +0 -94
- {omlish-0.0.0.dev123.dist-info → omlish-0.0.0.dev125.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev123.dist-info → omlish-0.0.0.dev125.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev123.dist-info → omlish-0.0.0.dev125.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,376 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import abc
|
3
|
+
import http.client
|
4
|
+
import http.server
|
5
|
+
import io
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from .versions import HttpProtocolVersion
|
9
|
+
from .versions import HttpProtocolVersions
|
10
|
+
|
11
|
+
|
12
|
+
T = ta.TypeVar('T')
|
13
|
+
|
14
|
+
|
15
|
+
HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
|
16
|
+
|
17
|
+
|
18
|
+
##
|
19
|
+
|
20
|
+
|
21
|
+
class ParseHttpRequestResult(abc.ABC): # noqa
|
22
|
+
__slots__ = (
|
23
|
+
'server_version',
|
24
|
+
'request_line',
|
25
|
+
'request_version',
|
26
|
+
'version',
|
27
|
+
'headers',
|
28
|
+
'close_connection',
|
29
|
+
)
|
30
|
+
|
31
|
+
def __init__(
|
32
|
+
self,
|
33
|
+
*,
|
34
|
+
server_version: HttpProtocolVersion,
|
35
|
+
request_line: str,
|
36
|
+
request_version: HttpProtocolVersion,
|
37
|
+
version: HttpProtocolVersion,
|
38
|
+
headers: ta.Optional[HttpHeaders],
|
39
|
+
close_connection: bool,
|
40
|
+
) -> None:
|
41
|
+
super().__init__()
|
42
|
+
|
43
|
+
self.server_version = server_version
|
44
|
+
self.request_line = request_line
|
45
|
+
self.request_version = request_version
|
46
|
+
self.version = version
|
47
|
+
self.headers = headers
|
48
|
+
self.close_connection = close_connection
|
49
|
+
|
50
|
+
def __repr__(self) -> str:
|
51
|
+
return f'{self.__class__.__name__}({", ".join(f"{a}={getattr(self, a)!r}" for a in self.__slots__)})'
|
52
|
+
|
53
|
+
|
54
|
+
class EmptyParsedHttpResult(ParseHttpRequestResult):
|
55
|
+
pass
|
56
|
+
|
57
|
+
|
58
|
+
class ParseHttpRequestError(ParseHttpRequestResult):
|
59
|
+
__slots__ = (
|
60
|
+
'code',
|
61
|
+
'message',
|
62
|
+
*ParseHttpRequestResult.__slots__,
|
63
|
+
)
|
64
|
+
|
65
|
+
def __init__(
|
66
|
+
self,
|
67
|
+
*,
|
68
|
+
code: http.HTTPStatus,
|
69
|
+
message: ta.Union[str, ta.Tuple[str, str]],
|
70
|
+
|
71
|
+
**kwargs: ta.Any,
|
72
|
+
) -> None:
|
73
|
+
super().__init__(**kwargs)
|
74
|
+
|
75
|
+
self.code = code
|
76
|
+
self.message = message
|
77
|
+
|
78
|
+
|
79
|
+
class ParsedHttpRequest(ParseHttpRequestResult):
|
80
|
+
__slots__ = (
|
81
|
+
'method',
|
82
|
+
'path',
|
83
|
+
'headers',
|
84
|
+
'expects_continue',
|
85
|
+
*[a for a in ParseHttpRequestResult.__slots__ if a != 'headers'],
|
86
|
+
)
|
87
|
+
|
88
|
+
def __init__(
|
89
|
+
self,
|
90
|
+
*,
|
91
|
+
method: str,
|
92
|
+
path: str,
|
93
|
+
headers: HttpHeaders,
|
94
|
+
expects_continue: bool,
|
95
|
+
|
96
|
+
**kwargs: ta.Any,
|
97
|
+
) -> None:
|
98
|
+
super().__init__(
|
99
|
+
headers=headers,
|
100
|
+
**kwargs,
|
101
|
+
)
|
102
|
+
|
103
|
+
self.method = method
|
104
|
+
self.path = path
|
105
|
+
self.expects_continue = expects_continue
|
106
|
+
|
107
|
+
headers: HttpHeaders
|
108
|
+
|
109
|
+
|
110
|
+
#
|
111
|
+
|
112
|
+
|
113
|
+
class HttpRequestParser:
|
114
|
+
DEFAULT_SERVER_VERSION = HttpProtocolVersions.HTTP_1_0
|
115
|
+
|
116
|
+
# The default request version. This only affects responses up until the point where the request line is parsed, so
|
117
|
+
# it mainly decides what the client gets back when sending a malformed request line.
|
118
|
+
# Most web servers default to HTTP 0.9, i.e. don't send a status line.
|
119
|
+
DEFAULT_REQUEST_VERSION = HttpProtocolVersions.HTTP_0_9
|
120
|
+
|
121
|
+
#
|
122
|
+
|
123
|
+
DEFAULT_MAX_LINE: int = 0x10000
|
124
|
+
DEFAULT_MAX_HEADERS: int = 100
|
125
|
+
|
126
|
+
#
|
127
|
+
|
128
|
+
def __init__(
|
129
|
+
self,
|
130
|
+
*,
|
131
|
+
server_version: HttpProtocolVersion = DEFAULT_SERVER_VERSION,
|
132
|
+
|
133
|
+
max_line: int = DEFAULT_MAX_LINE,
|
134
|
+
max_headers: int = DEFAULT_MAX_HEADERS,
|
135
|
+
) -> None:
|
136
|
+
super().__init__()
|
137
|
+
|
138
|
+
if server_version >= HttpProtocolVersions.HTTP_2_0:
|
139
|
+
raise ValueError(f'Unsupported protocol version: {server_version}')
|
140
|
+
self._server_version = server_version
|
141
|
+
|
142
|
+
self._max_line = max_line
|
143
|
+
self._max_headers = max_headers
|
144
|
+
|
145
|
+
#
|
146
|
+
|
147
|
+
@property
|
148
|
+
def server_version(self) -> HttpProtocolVersion:
|
149
|
+
return self._server_version
|
150
|
+
|
151
|
+
#
|
152
|
+
|
153
|
+
def _run_read_line_coro(
|
154
|
+
self,
|
155
|
+
gen: ta.Generator[int, bytes, T],
|
156
|
+
read_line: ta.Callable[[int], bytes],
|
157
|
+
) -> T:
|
158
|
+
sz = next(gen)
|
159
|
+
while True:
|
160
|
+
try:
|
161
|
+
sz = gen.send(read_line(sz))
|
162
|
+
except StopIteration as e:
|
163
|
+
return e.value
|
164
|
+
|
165
|
+
#
|
166
|
+
|
167
|
+
def parse_request_version(self, version_str: str) -> HttpProtocolVersion:
|
168
|
+
if not version_str.startswith('HTTP/'):
|
169
|
+
raise ValueError(version_str) # noqa
|
170
|
+
|
171
|
+
base_version_number = version_str.split('/', 1)[1]
|
172
|
+
version_number_parts = base_version_number.split('.')
|
173
|
+
|
174
|
+
# RFC 2145 section 3.1 says there can be only one "." and
|
175
|
+
# - major and minor numbers MUST be treated as separate integers;
|
176
|
+
# - HTTP/2.4 is a lower version than HTTP/2.13, which in turn is lower than HTTP/12.3;
|
177
|
+
# - Leading zeros MUST be ignored by recipients.
|
178
|
+
if len(version_number_parts) != 2:
|
179
|
+
raise ValueError(version_number_parts) # noqa
|
180
|
+
if any(not component.isdigit() for component in version_number_parts):
|
181
|
+
raise ValueError('non digit in http version') # noqa
|
182
|
+
if any(len(component) > 10 for component in version_number_parts):
|
183
|
+
raise ValueError('unreasonable length http version') # noqa
|
184
|
+
|
185
|
+
return HttpProtocolVersion(
|
186
|
+
int(version_number_parts[0]),
|
187
|
+
int(version_number_parts[1]),
|
188
|
+
)
|
189
|
+
|
190
|
+
#
|
191
|
+
|
192
|
+
def coro_read_raw_headers(self) -> ta.Generator[int, bytes, ta.List[bytes]]:
|
193
|
+
raw_headers: ta.List[bytes] = []
|
194
|
+
while True:
|
195
|
+
line = yield self._max_line + 1
|
196
|
+
if len(line) > self._max_line:
|
197
|
+
raise http.client.LineTooLong('header line')
|
198
|
+
raw_headers.append(line)
|
199
|
+
if len(raw_headers) > self._max_headers:
|
200
|
+
raise http.client.HTTPException(f'got more than {self._max_headers} headers')
|
201
|
+
if line in (b'\r\n', b'\n', b''):
|
202
|
+
break
|
203
|
+
return raw_headers
|
204
|
+
|
205
|
+
def read_raw_headers(self, read_line: ta.Callable[[int], bytes]) -> ta.List[bytes]:
|
206
|
+
return self._run_read_line_coro(self.coro_read_raw_headers(), read_line)
|
207
|
+
|
208
|
+
def parse_raw_headers(self, raw_headers: ta.Sequence[bytes]) -> HttpHeaders:
|
209
|
+
return http.client.parse_headers(io.BytesIO(b''.join(raw_headers)))
|
210
|
+
|
211
|
+
#
|
212
|
+
|
213
|
+
def coro_parse(self) -> ta.Generator[int, bytes, ParseHttpRequestResult]:
|
214
|
+
raw_request_line = yield self._max_line + 1
|
215
|
+
|
216
|
+
# Common result kwargs
|
217
|
+
|
218
|
+
request_line = '-'
|
219
|
+
request_version = self.DEFAULT_REQUEST_VERSION
|
220
|
+
|
221
|
+
# Set to min(server, request) when it gets that far, but if it fails before that the server authoritatively
|
222
|
+
# responds with its own version.
|
223
|
+
version = self._server_version
|
224
|
+
|
225
|
+
headers: HttpHeaders | None = None
|
226
|
+
|
227
|
+
close_connection = True
|
228
|
+
|
229
|
+
def result_kwargs():
|
230
|
+
return dict(
|
231
|
+
server_version=self._server_version,
|
232
|
+
request_line=request_line,
|
233
|
+
request_version=request_version,
|
234
|
+
version=version,
|
235
|
+
headers=headers,
|
236
|
+
close_connection=close_connection,
|
237
|
+
)
|
238
|
+
|
239
|
+
# Decode line
|
240
|
+
|
241
|
+
if len(raw_request_line) > self._max_line:
|
242
|
+
return ParseHttpRequestError(
|
243
|
+
code=http.HTTPStatus.REQUEST_URI_TOO_LONG,
|
244
|
+
message='Request line too long',
|
245
|
+
**result_kwargs(),
|
246
|
+
)
|
247
|
+
|
248
|
+
if not raw_request_line:
|
249
|
+
return EmptyParsedHttpResult(**result_kwargs())
|
250
|
+
|
251
|
+
request_line = raw_request_line.decode('iso-8859-1').rstrip('\r\n')
|
252
|
+
|
253
|
+
# Split words
|
254
|
+
|
255
|
+
words = request_line.split()
|
256
|
+
if len(words) == 0:
|
257
|
+
return EmptyParsedHttpResult(**result_kwargs())
|
258
|
+
|
259
|
+
# Parse and set version
|
260
|
+
|
261
|
+
if len(words) >= 3: # Enough to determine protocol version
|
262
|
+
version_str = words[-1]
|
263
|
+
try:
|
264
|
+
request_version = self.parse_request_version(version_str)
|
265
|
+
|
266
|
+
except (ValueError, IndexError):
|
267
|
+
return ParseHttpRequestError(
|
268
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
269
|
+
message=f'Bad request version ({version_str!r})',
|
270
|
+
**result_kwargs(),
|
271
|
+
)
|
272
|
+
|
273
|
+
if (
|
274
|
+
request_version < HttpProtocolVersions.HTTP_0_9 or
|
275
|
+
request_version >= HttpProtocolVersions.HTTP_2_0
|
276
|
+
):
|
277
|
+
return ParseHttpRequestError(
|
278
|
+
code=http.HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
|
279
|
+
message=f'Invalid HTTP version ({version_str})',
|
280
|
+
**result_kwargs(),
|
281
|
+
)
|
282
|
+
|
283
|
+
version = min([self._server_version, request_version])
|
284
|
+
|
285
|
+
if version >= HttpProtocolVersions.HTTP_1_1:
|
286
|
+
close_connection = False
|
287
|
+
|
288
|
+
# Verify word count
|
289
|
+
|
290
|
+
if not 2 <= len(words) <= 3:
|
291
|
+
return ParseHttpRequestError(
|
292
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
293
|
+
message=f'Bad request syntax ({request_line!r})',
|
294
|
+
**result_kwargs(),
|
295
|
+
)
|
296
|
+
|
297
|
+
# Parse method and path
|
298
|
+
|
299
|
+
method, path = words[:2]
|
300
|
+
if len(words) == 2:
|
301
|
+
close_connection = True
|
302
|
+
if method != 'GET':
|
303
|
+
return ParseHttpRequestError(
|
304
|
+
code=http.HTTPStatus.BAD_REQUEST,
|
305
|
+
message=f'Bad HTTP/0.9 request type ({method!r})',
|
306
|
+
**result_kwargs(),
|
307
|
+
)
|
308
|
+
|
309
|
+
# gh-87389: The purpose of replacing '//' with '/' is to protect against open redirect attacks possibly
|
310
|
+
# triggered if the path starts with '//' because http clients treat //path as an absolute URI without scheme
|
311
|
+
# (similar to http://path) rather than a path.
|
312
|
+
if path.startswith('//'):
|
313
|
+
path = '/' + path.lstrip('/') # Reduce to a single /
|
314
|
+
|
315
|
+
# Parse headers
|
316
|
+
|
317
|
+
try:
|
318
|
+
raw_gen = self.coro_read_raw_headers()
|
319
|
+
raw_sz = next(raw_gen)
|
320
|
+
while True:
|
321
|
+
buf = yield raw_sz
|
322
|
+
try:
|
323
|
+
raw_sz = raw_gen.send(buf)
|
324
|
+
except StopIteration as e:
|
325
|
+
raw_headers = e.value
|
326
|
+
break
|
327
|
+
|
328
|
+
headers = self.parse_raw_headers(raw_headers)
|
329
|
+
|
330
|
+
except http.client.LineTooLong as err:
|
331
|
+
return ParseHttpRequestError(
|
332
|
+
code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
|
333
|
+
message=('Line too long', str(err)),
|
334
|
+
**result_kwargs(),
|
335
|
+
)
|
336
|
+
|
337
|
+
except http.client.HTTPException as err:
|
338
|
+
return ParseHttpRequestError(
|
339
|
+
code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
|
340
|
+
message=('Too many headers', str(err)),
|
341
|
+
**result_kwargs(),
|
342
|
+
)
|
343
|
+
|
344
|
+
# Check for connection directive
|
345
|
+
|
346
|
+
conn_type = headers.get('Connection', '')
|
347
|
+
if conn_type.lower() == 'close':
|
348
|
+
close_connection = True
|
349
|
+
elif (
|
350
|
+
conn_type.lower() == 'keep-alive' and
|
351
|
+
version >= HttpProtocolVersions.HTTP_1_1
|
352
|
+
):
|
353
|
+
close_connection = False
|
354
|
+
|
355
|
+
# Check for expect directive
|
356
|
+
|
357
|
+
expect = headers.get('Expect', '')
|
358
|
+
if (
|
359
|
+
expect.lower() == '100-continue' and
|
360
|
+
version >= HttpProtocolVersions.HTTP_1_1
|
361
|
+
):
|
362
|
+
expects_continue = True
|
363
|
+
else:
|
364
|
+
expects_continue = False
|
365
|
+
|
366
|
+
# Return
|
367
|
+
|
368
|
+
return ParsedHttpRequest(
|
369
|
+
method=method,
|
370
|
+
path=path,
|
371
|
+
expects_continue=expects_continue,
|
372
|
+
**result_kwargs(),
|
373
|
+
)
|
374
|
+
|
375
|
+
def parse(self, read_line: ta.Callable[[int], bytes]) -> ParseHttpRequestResult:
|
376
|
+
return self._run_read_line_coro(self.coro_parse(), read_line)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import typing as ta
|
3
|
+
|
4
|
+
|
5
|
+
class HttpProtocolVersion(ta.NamedTuple):
|
6
|
+
major: int
|
7
|
+
minor: int
|
8
|
+
|
9
|
+
def __str__(self) -> str:
|
10
|
+
return f'HTTP/{self.major}.{self.minor}'
|
11
|
+
|
12
|
+
|
13
|
+
class HttpProtocolVersions:
|
14
|
+
HTTP_0_9 = HttpProtocolVersion(0, 9)
|
15
|
+
HTTP_1_0 = HttpProtocolVersion(1, 0)
|
16
|
+
HTTP_1_1 = HttpProtocolVersion(1, 1)
|
17
|
+
HTTP_2_0 = HttpProtocolVersion(2, 0)
|