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.
@@ -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