omlish 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev125__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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,585 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
3
|
+
# --------------------------------------------
|
4
|
+
#
|
5
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
6
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
7
|
+
# documentation.
|
8
|
+
#
|
9
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
10
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
11
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
12
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
13
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
|
14
|
+
# alone or in any derivative version prepared by Licensee.
|
15
|
+
#
|
16
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
17
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
18
|
+
# any such work a brief summary of the changes made to Python.
|
19
|
+
#
|
20
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
21
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
22
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
23
|
+
# RIGHTS.
|
24
|
+
#
|
25
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
26
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
27
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
28
|
+
#
|
29
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
30
|
+
#
|
31
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
32
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
33
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
34
|
+
#
|
35
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
36
|
+
# License Agreement.
|
37
|
+
"""
|
38
|
+
"Test suite" lol:
|
39
|
+
|
40
|
+
curl -v localhost:8000
|
41
|
+
curl -v localhost:8000 -d 'foo'
|
42
|
+
curl -v -XFOO localhost:8000 -d 'foo'
|
43
|
+
curl -v -XPOST -H 'Expect: 100-Continue' localhost:8000 -d 'foo'
|
44
|
+
|
45
|
+
curl -v -0 localhost:8000
|
46
|
+
curl -v -0 localhost:8000 -d 'foo'
|
47
|
+
curl -v -0 -XFOO localhost:8000 -d 'foo'
|
48
|
+
|
49
|
+
curl -v -XPOST localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
|
50
|
+
curl -v -XPOST localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
|
51
|
+
curl -v -XFOO localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
|
52
|
+
curl -v -XFOO localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
|
53
|
+
"""
|
54
|
+
import abc
|
55
|
+
import dataclasses as dc
|
56
|
+
import email.utils
|
57
|
+
import html
|
58
|
+
import http.client
|
59
|
+
import http.server
|
60
|
+
import io
|
61
|
+
import textwrap
|
62
|
+
import time
|
63
|
+
import typing as ta
|
64
|
+
|
65
|
+
from ..check import check_isinstance
|
66
|
+
from ..check import check_none
|
67
|
+
from ..check import check_not_none
|
68
|
+
from ..socket import SocketAddress
|
69
|
+
from ..socket import SocketHandler
|
70
|
+
from .handlers import HttpHandler
|
71
|
+
from .handlers import HttpHandlerRequest
|
72
|
+
from .handlers import UnsupportedMethodHttpHandlerError
|
73
|
+
from .parsing import EmptyParsedHttpResult
|
74
|
+
from .parsing import HttpRequestParser
|
75
|
+
from .parsing import ParsedHttpRequest
|
76
|
+
from .parsing import ParseHttpRequestError
|
77
|
+
from .versions import HttpProtocolVersion
|
78
|
+
from .versions import HttpProtocolVersions
|
79
|
+
|
80
|
+
|
81
|
+
CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
|
82
|
+
|
83
|
+
|
84
|
+
##
|
85
|
+
|
86
|
+
|
87
|
+
class CoroHttpServer:
|
88
|
+
"""
|
89
|
+
Adapted from stdlib:
|
90
|
+
- https://github.com/python/cpython/blob/4b4e0dbdf49adc91c35a357ad332ab3abd4c31b1/Lib/http/server.py#L146
|
91
|
+
"""
|
92
|
+
|
93
|
+
#
|
94
|
+
|
95
|
+
def __init__(
|
96
|
+
self,
|
97
|
+
client_address: SocketAddress,
|
98
|
+
*,
|
99
|
+
handler: HttpHandler,
|
100
|
+
parser: HttpRequestParser = HttpRequestParser(),
|
101
|
+
|
102
|
+
default_content_type: ta.Optional[str] = None,
|
103
|
+
|
104
|
+
error_message_format: ta.Optional[str] = None,
|
105
|
+
error_content_type: ta.Optional[str] = None,
|
106
|
+
) -> None:
|
107
|
+
super().__init__()
|
108
|
+
|
109
|
+
self._client_address = client_address
|
110
|
+
|
111
|
+
self._handler = handler
|
112
|
+
self._parser = parser
|
113
|
+
|
114
|
+
self._default_content_type = default_content_type or self.DEFAULT_CONTENT_TYPE
|
115
|
+
|
116
|
+
self._error_message_format = error_message_format or self.DEFAULT_ERROR_MESSAGE
|
117
|
+
self._error_content_type = error_content_type or self.DEFAULT_ERROR_CONTENT_TYPE
|
118
|
+
|
119
|
+
#
|
120
|
+
|
121
|
+
@property
|
122
|
+
def client_address(self) -> SocketAddress:
|
123
|
+
return self._client_address
|
124
|
+
|
125
|
+
@property
|
126
|
+
def handler(self) -> HttpHandler:
|
127
|
+
return self._handler
|
128
|
+
|
129
|
+
@property
|
130
|
+
def parser(self) -> HttpRequestParser:
|
131
|
+
return self._parser
|
132
|
+
|
133
|
+
#
|
134
|
+
|
135
|
+
def _format_timestamp(self, timestamp: ta.Optional[float] = None) -> str:
|
136
|
+
if timestamp is None:
|
137
|
+
timestamp = time.time()
|
138
|
+
return email.utils.formatdate(timestamp, usegmt=True)
|
139
|
+
|
140
|
+
#
|
141
|
+
|
142
|
+
def _header_encode(self, s: str) -> bytes:
|
143
|
+
return s.encode('latin-1', 'strict')
|
144
|
+
|
145
|
+
class _Header(ta.NamedTuple):
|
146
|
+
key: str
|
147
|
+
value: str
|
148
|
+
|
149
|
+
def _format_header_line(self, h: _Header) -> str:
|
150
|
+
return f'{h.key}: {h.value}\r\n'
|
151
|
+
|
152
|
+
def _get_header_close_connection_action(self, h: _Header) -> ta.Optional[bool]:
|
153
|
+
if h.key.lower() != 'connection':
|
154
|
+
return None
|
155
|
+
elif h.value.lower() == 'close':
|
156
|
+
return True
|
157
|
+
elif h.value.lower() == 'keep-alive':
|
158
|
+
return False
|
159
|
+
else:
|
160
|
+
return None
|
161
|
+
|
162
|
+
def _make_default_headers(self) -> ta.List[_Header]:
|
163
|
+
return [
|
164
|
+
self._Header('Date', self._format_timestamp()),
|
165
|
+
]
|
166
|
+
|
167
|
+
#
|
168
|
+
|
169
|
+
_STATUS_RESPONSES: ta.Mapping[int, ta.Tuple[str, str]] = {
|
170
|
+
v: (v.phrase, v.description)
|
171
|
+
for v in http.HTTPStatus.__members__.values()
|
172
|
+
}
|
173
|
+
|
174
|
+
def _format_status_line(
|
175
|
+
self,
|
176
|
+
version: HttpProtocolVersion,
|
177
|
+
code: ta.Union[http.HTTPStatus, int],
|
178
|
+
message: ta.Optional[str] = None,
|
179
|
+
) -> str:
|
180
|
+
if message is None:
|
181
|
+
if code in self._STATUS_RESPONSES:
|
182
|
+
message = self._STATUS_RESPONSES[code][0]
|
183
|
+
else:
|
184
|
+
message = ''
|
185
|
+
|
186
|
+
return f'{version} {int(code)} {message}\r\n'
|
187
|
+
|
188
|
+
#
|
189
|
+
|
190
|
+
@dc.dataclass(frozen=True)
|
191
|
+
class _Response:
|
192
|
+
version: HttpProtocolVersion
|
193
|
+
code: http.HTTPStatus
|
194
|
+
|
195
|
+
message: ta.Optional[str] = None
|
196
|
+
headers: ta.Optional[ta.Sequence['CoroHttpServer._Header']] = None
|
197
|
+
data: ta.Optional[bytes] = None
|
198
|
+
close_connection: ta.Optional[bool] = False
|
199
|
+
|
200
|
+
def get_header(self, key: str) -> ta.Optional['CoroHttpServer._Header']:
|
201
|
+
for h in self.headers or []:
|
202
|
+
if h.key.lower() == key.lower():
|
203
|
+
return h
|
204
|
+
return None
|
205
|
+
|
206
|
+
#
|
207
|
+
|
208
|
+
def _build_response_bytes(self, a: _Response) -> bytes:
|
209
|
+
out = io.BytesIO()
|
210
|
+
|
211
|
+
if a.version >= HttpProtocolVersions.HTTP_1_0:
|
212
|
+
out.write(self._header_encode(self._format_status_line(
|
213
|
+
a.version,
|
214
|
+
a.code,
|
215
|
+
a.message,
|
216
|
+
)))
|
217
|
+
|
218
|
+
for h in a.headers or []:
|
219
|
+
out.write(self._header_encode(self._format_header_line(h)))
|
220
|
+
|
221
|
+
out.write(b'\r\n')
|
222
|
+
|
223
|
+
if a.data is not None:
|
224
|
+
out.write(a.data)
|
225
|
+
|
226
|
+
return out.getvalue()
|
227
|
+
|
228
|
+
#
|
229
|
+
|
230
|
+
DEFAULT_CONTENT_TYPE = 'text/plain'
|
231
|
+
|
232
|
+
def _preprocess_response(self, resp: _Response) -> _Response:
|
233
|
+
nh: ta.List[CoroHttpServer._Header] = []
|
234
|
+
kw: ta.Dict[str, ta.Any] = {}
|
235
|
+
|
236
|
+
if resp.get_header('Content-Type') is None:
|
237
|
+
nh.append(self._Header('Content-Type', self._default_content_type))
|
238
|
+
if resp.data is not None and resp.get_header('Content-Length') is None:
|
239
|
+
nh.append(self._Header('Content-Length', str(len(resp.data))))
|
240
|
+
|
241
|
+
if nh:
|
242
|
+
kw.update(headers=[*(resp.headers or []), *nh])
|
243
|
+
|
244
|
+
if (clh := resp.get_header('Connection')) is not None:
|
245
|
+
if self._get_header_close_connection_action(clh):
|
246
|
+
kw.update(close_connection=True)
|
247
|
+
|
248
|
+
if not kw:
|
249
|
+
return resp
|
250
|
+
return dc.replace(resp, **kw)
|
251
|
+
|
252
|
+
#
|
253
|
+
|
254
|
+
@dc.dataclass(frozen=True)
|
255
|
+
class Error:
|
256
|
+
version: HttpProtocolVersion
|
257
|
+
code: http.HTTPStatus
|
258
|
+
message: str
|
259
|
+
explain: str
|
260
|
+
|
261
|
+
method: ta.Optional[str] = None
|
262
|
+
|
263
|
+
def _build_error(
|
264
|
+
self,
|
265
|
+
code: ta.Union[http.HTTPStatus, int],
|
266
|
+
message: ta.Optional[str] = None,
|
267
|
+
explain: ta.Optional[str] = None,
|
268
|
+
*,
|
269
|
+
version: ta.Optional[HttpProtocolVersion] = None,
|
270
|
+
method: ta.Optional[str] = None,
|
271
|
+
) -> Error:
|
272
|
+
code = http.HTTPStatus(code)
|
273
|
+
|
274
|
+
try:
|
275
|
+
short_msg, long_msg = self._STATUS_RESPONSES[code]
|
276
|
+
except KeyError:
|
277
|
+
short_msg, long_msg = '???', '???'
|
278
|
+
if message is None:
|
279
|
+
message = short_msg
|
280
|
+
if explain is None:
|
281
|
+
explain = long_msg
|
282
|
+
|
283
|
+
if version is None:
|
284
|
+
version = self._parser.server_version
|
285
|
+
|
286
|
+
return self.Error(
|
287
|
+
version=version,
|
288
|
+
code=code,
|
289
|
+
message=message,
|
290
|
+
explain=explain,
|
291
|
+
|
292
|
+
method=method,
|
293
|
+
)
|
294
|
+
|
295
|
+
#
|
296
|
+
|
297
|
+
DEFAULT_ERROR_MESSAGE = textwrap.dedent("""\
|
298
|
+
<!DOCTYPE HTML>
|
299
|
+
<html lang="en">
|
300
|
+
<head>
|
301
|
+
<meta charset="utf-8">
|
302
|
+
<title>Error response</title>
|
303
|
+
</head>
|
304
|
+
<body>
|
305
|
+
<h1>Error response</h1>
|
306
|
+
<p>Error code: %(code)d</p>
|
307
|
+
<p>Message: %(message)s.</p>
|
308
|
+
<p>Error code explanation: %(code)s - %(explain)s.</p>
|
309
|
+
</body>
|
310
|
+
</html>
|
311
|
+
""")
|
312
|
+
|
313
|
+
DEFAULT_ERROR_CONTENT_TYPE = 'text/html;charset=utf-8'
|
314
|
+
|
315
|
+
def _build_error_response(self, err: Error) -> _Response:
|
316
|
+
headers: ta.List[CoroHttpServer._Header] = [
|
317
|
+
*self._make_default_headers(),
|
318
|
+
self._Header('Connection', 'close'),
|
319
|
+
]
|
320
|
+
|
321
|
+
# Message body is omitted for cases described in:
|
322
|
+
# - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
|
323
|
+
# - RFC7231: 6.3.6. 205(Reset Content)
|
324
|
+
data: ta.Optional[bytes] = None
|
325
|
+
if (
|
326
|
+
err.code >= http.HTTPStatus.OK and
|
327
|
+
err.code not in (
|
328
|
+
http.HTTPStatus.NO_CONTENT,
|
329
|
+
http.HTTPStatus.RESET_CONTENT,
|
330
|
+
http.HTTPStatus.NOT_MODIFIED,
|
331
|
+
)
|
332
|
+
):
|
333
|
+
# HTML encode to prevent Cross Site Scripting attacks (see bug #1100201)
|
334
|
+
content = self._error_message_format.format(
|
335
|
+
code=err.code,
|
336
|
+
message=html.escape(err.message, quote=False),
|
337
|
+
explain=html.escape(err.explain, quote=False),
|
338
|
+
)
|
339
|
+
body = content.encode('UTF-8', 'replace')
|
340
|
+
|
341
|
+
headers.extend([
|
342
|
+
self._Header('Content-Type', self._error_content_type),
|
343
|
+
self._Header('Content-Length', str(len(body))),
|
344
|
+
])
|
345
|
+
|
346
|
+
if err.method != 'HEAD' and body:
|
347
|
+
data = body
|
348
|
+
|
349
|
+
return self._Response(
|
350
|
+
version=err.version,
|
351
|
+
code=err.code,
|
352
|
+
message=err.message,
|
353
|
+
headers=headers,
|
354
|
+
data=data,
|
355
|
+
close_connection=True,
|
356
|
+
)
|
357
|
+
|
358
|
+
#
|
359
|
+
|
360
|
+
class Io(abc.ABC): # noqa
|
361
|
+
pass
|
362
|
+
|
363
|
+
#
|
364
|
+
|
365
|
+
class AnyLogIo(Io):
|
366
|
+
pass
|
367
|
+
|
368
|
+
@dc.dataclass(frozen=True)
|
369
|
+
class ParsedRequestLogIo(AnyLogIo):
|
370
|
+
request: ParsedHttpRequest
|
371
|
+
|
372
|
+
@dc.dataclass(frozen=True)
|
373
|
+
class ErrorLogIo(AnyLogIo):
|
374
|
+
error: 'CoroHttpServer.Error'
|
375
|
+
|
376
|
+
#
|
377
|
+
|
378
|
+
class AnyReadIo(Io): # noqa
|
379
|
+
pass
|
380
|
+
|
381
|
+
@dc.dataclass(frozen=True)
|
382
|
+
class ReadIo(AnyReadIo):
|
383
|
+
sz: int
|
384
|
+
|
385
|
+
@dc.dataclass(frozen=True)
|
386
|
+
class ReadLineIo(AnyReadIo):
|
387
|
+
sz: int
|
388
|
+
|
389
|
+
#
|
390
|
+
|
391
|
+
@dc.dataclass(frozen=True)
|
392
|
+
class WriteIo(Io):
|
393
|
+
data: bytes
|
394
|
+
|
395
|
+
#
|
396
|
+
|
397
|
+
def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
|
398
|
+
while True:
|
399
|
+
gen = self.coro_handle_one()
|
400
|
+
|
401
|
+
o = next(gen)
|
402
|
+
i: ta.Optional[bytes]
|
403
|
+
while True:
|
404
|
+
if isinstance(o, self.AnyLogIo):
|
405
|
+
i = None
|
406
|
+
yield o
|
407
|
+
|
408
|
+
elif isinstance(o, self.AnyReadIo):
|
409
|
+
i = check_isinstance((yield o), bytes)
|
410
|
+
|
411
|
+
elif isinstance(o, self._Response):
|
412
|
+
i = None
|
413
|
+
r = self._preprocess_response(o)
|
414
|
+
b = self._build_response_bytes(r)
|
415
|
+
check_none((yield self.WriteIo(b)))
|
416
|
+
|
417
|
+
else:
|
418
|
+
raise TypeError(o)
|
419
|
+
|
420
|
+
try:
|
421
|
+
o = gen.send(i)
|
422
|
+
except EOFError:
|
423
|
+
return
|
424
|
+
except StopIteration:
|
425
|
+
break
|
426
|
+
|
427
|
+
def coro_handle_one(self) -> ta.Generator[
|
428
|
+
ta.Union[AnyLogIo, AnyReadIo, _Response],
|
429
|
+
ta.Optional[bytes],
|
430
|
+
None,
|
431
|
+
]:
|
432
|
+
# Parse request
|
433
|
+
|
434
|
+
gen = self._parser.coro_parse()
|
435
|
+
sz = next(gen)
|
436
|
+
while True:
|
437
|
+
try:
|
438
|
+
line = check_isinstance((yield self.ReadLineIo(sz)), bytes)
|
439
|
+
sz = gen.send(line)
|
440
|
+
except StopIteration as e:
|
441
|
+
parsed = e.value
|
442
|
+
break
|
443
|
+
|
444
|
+
if isinstance(parsed, EmptyParsedHttpResult):
|
445
|
+
raise EOFError # noqa
|
446
|
+
|
447
|
+
if isinstance(parsed, ParseHttpRequestError):
|
448
|
+
err = self._build_error(
|
449
|
+
parsed.code,
|
450
|
+
*parsed.message,
|
451
|
+
version=parsed.version,
|
452
|
+
)
|
453
|
+
yield self.ErrorLogIo(err)
|
454
|
+
yield self._build_error_response(err)
|
455
|
+
return
|
456
|
+
|
457
|
+
parsed = check_isinstance(parsed, ParsedHttpRequest)
|
458
|
+
|
459
|
+
# Log
|
460
|
+
|
461
|
+
check_none((yield self.ParsedRequestLogIo(parsed)))
|
462
|
+
|
463
|
+
# Handle CONTINUE
|
464
|
+
|
465
|
+
if parsed.expects_continue:
|
466
|
+
# https://bugs.python.org/issue1491
|
467
|
+
# https://github.com/python/cpython/commit/0f476d49f8d4aa84210392bf13b59afc67b32b31
|
468
|
+
yield self._Response(
|
469
|
+
version=parsed.version,
|
470
|
+
code=http.HTTPStatus.CONTINUE,
|
471
|
+
)
|
472
|
+
|
473
|
+
# Read data
|
474
|
+
|
475
|
+
request_data: ta.Optional[bytes]
|
476
|
+
if (cl := parsed.headers.get('Content-Length')) is not None:
|
477
|
+
request_data = check_isinstance((yield self.ReadIo(int(cl))), bytes)
|
478
|
+
else:
|
479
|
+
request_data = None
|
480
|
+
|
481
|
+
# Build request
|
482
|
+
|
483
|
+
handler_request = HttpHandlerRequest(
|
484
|
+
client_address=self._client_address,
|
485
|
+
method=check_not_none(parsed.method),
|
486
|
+
path=parsed.path,
|
487
|
+
headers=parsed.headers,
|
488
|
+
data=request_data,
|
489
|
+
)
|
490
|
+
|
491
|
+
# Build handler response
|
492
|
+
|
493
|
+
try:
|
494
|
+
handler_response = self._handler(handler_request)
|
495
|
+
|
496
|
+
except UnsupportedMethodHttpHandlerError:
|
497
|
+
err = self._build_error(
|
498
|
+
http.HTTPStatus.NOT_IMPLEMENTED,
|
499
|
+
f'Unsupported method ({parsed.method!r})',
|
500
|
+
version=parsed.version,
|
501
|
+
method=parsed.method,
|
502
|
+
)
|
503
|
+
yield self.ErrorLogIo(err)
|
504
|
+
yield self._build_error_response(err)
|
505
|
+
return
|
506
|
+
|
507
|
+
# Build internal response
|
508
|
+
|
509
|
+
response_headers = handler_response.headers or {}
|
510
|
+
response_data = handler_response.data
|
511
|
+
|
512
|
+
headers: ta.List[CoroHttpServer._Header] = [
|
513
|
+
*self._make_default_headers(),
|
514
|
+
]
|
515
|
+
|
516
|
+
for k, v in response_headers.items():
|
517
|
+
headers.append(self._Header(k, v))
|
518
|
+
|
519
|
+
if handler_response.close_connection and 'Connection' not in headers:
|
520
|
+
headers.append(self._Header('Connection', 'close'))
|
521
|
+
|
522
|
+
yield self._Response(
|
523
|
+
version=parsed.version,
|
524
|
+
code=http.HTTPStatus(handler_response.status),
|
525
|
+
headers=headers,
|
526
|
+
data=response_data,
|
527
|
+
close_connection=handler_response.close_connection,
|
528
|
+
)
|
529
|
+
|
530
|
+
|
531
|
+
##
|
532
|
+
|
533
|
+
|
534
|
+
class CoroHttpServerSocketHandler(SocketHandler):
|
535
|
+
def __init__(
|
536
|
+
self,
|
537
|
+
client_address: SocketAddress,
|
538
|
+
rfile: ta.BinaryIO,
|
539
|
+
wfile: ta.BinaryIO,
|
540
|
+
*,
|
541
|
+
server_factory: CoroHttpServerFactory,
|
542
|
+
log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
|
543
|
+
) -> None:
|
544
|
+
super().__init__(
|
545
|
+
client_address,
|
546
|
+
rfile,
|
547
|
+
wfile,
|
548
|
+
)
|
549
|
+
|
550
|
+
self._server_factory = server_factory
|
551
|
+
self._log_handler = log_handler
|
552
|
+
|
553
|
+
def handle(self) -> None:
|
554
|
+
server = self._server_factory(self._client_address)
|
555
|
+
|
556
|
+
gen = server.coro_handle()
|
557
|
+
|
558
|
+
o = next(gen)
|
559
|
+
while True:
|
560
|
+
if isinstance(o, CoroHttpServer.AnyLogIo):
|
561
|
+
i = None
|
562
|
+
if self._log_handler is not None:
|
563
|
+
self._log_handler(server, o)
|
564
|
+
|
565
|
+
elif isinstance(o, CoroHttpServer.ReadIo):
|
566
|
+
i = self._rfile.read(o.sz)
|
567
|
+
|
568
|
+
elif isinstance(o, CoroHttpServer.ReadLineIo):
|
569
|
+
i = self._rfile.readline(o.sz)
|
570
|
+
|
571
|
+
elif isinstance(o, CoroHttpServer.WriteIo):
|
572
|
+
i = None
|
573
|
+
self._wfile.write(o.data)
|
574
|
+
self._wfile.flush()
|
575
|
+
|
576
|
+
else:
|
577
|
+
raise TypeError(o)
|
578
|
+
|
579
|
+
try:
|
580
|
+
if i is not None:
|
581
|
+
o = gen.send(i)
|
582
|
+
else:
|
583
|
+
o = next(gen)
|
584
|
+
except StopIteration:
|
585
|
+
break
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
3
|
+
import http.server
|
4
|
+
import typing as ta
|
5
|
+
|
6
|
+
from ..socket import SocketAddress
|
7
|
+
from .parsing import HttpHeaders
|
8
|
+
|
9
|
+
|
10
|
+
HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse']
|
11
|
+
|
12
|
+
|
13
|
+
@dc.dataclass(frozen=True)
|
14
|
+
class HttpHandlerRequest:
|
15
|
+
client_address: SocketAddress
|
16
|
+
method: str
|
17
|
+
path: str
|
18
|
+
headers: HttpHeaders
|
19
|
+
data: ta.Optional[bytes]
|
20
|
+
|
21
|
+
|
22
|
+
@dc.dataclass(frozen=True)
|
23
|
+
class HttpHandlerResponse:
|
24
|
+
status: ta.Union[http.HTTPStatus, int]
|
25
|
+
|
26
|
+
headers: ta.Optional[ta.Mapping[str, str]] = None
|
27
|
+
data: ta.Optional[bytes] = None
|
28
|
+
close_connection: ta.Optional[bool] = None
|
29
|
+
|
30
|
+
|
31
|
+
class HttpHandlerError(Exception):
|
32
|
+
pass
|
33
|
+
|
34
|
+
|
35
|
+
class UnsupportedMethodHttpHandlerError(Exception):
|
36
|
+
pass
|