tigrcorn-transports 0.3.16.dev5__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.
Files changed (45) hide show
  1. tigrcorn_transports/__init__.py +1 -0
  2. tigrcorn_transports/base.py +9 -0
  3. tigrcorn_transports/inproc/__init__.py +3 -0
  4. tigrcorn_transports/inproc/channel.py +19 -0
  5. tigrcorn_transports/listeners/__init__.py +4 -0
  6. tigrcorn_transports/listeners/base.py +14 -0
  7. tigrcorn_transports/listeners/inproc.py +24 -0
  8. tigrcorn_transports/listeners/pipe.py +72 -0
  9. tigrcorn_transports/listeners/registry.py +15 -0
  10. tigrcorn_transports/listeners/tcp.py +90 -0
  11. tigrcorn_transports/listeners/udp.py +82 -0
  12. tigrcorn_transports/listeners/unix.py +69 -0
  13. tigrcorn_transports/pipe/__init__.py +3 -0
  14. tigrcorn_transports/pipe/connection.py +19 -0
  15. tigrcorn_transports/py.typed +1 -0
  16. tigrcorn_transports/quic/__init__.py +108 -0
  17. tigrcorn_transports/quic/connection.py +2178 -0
  18. tigrcorn_transports/quic/crypto.py +508 -0
  19. tigrcorn_transports/quic/datagrams.py +72 -0
  20. tigrcorn_transports/quic/flow.py +168 -0
  21. tigrcorn_transports/quic/handshake.py +21 -0
  22. tigrcorn_transports/quic/packets.py +398 -0
  23. tigrcorn_transports/quic/recovery.py +435 -0
  24. tigrcorn_transports/quic/scheduler.py +70 -0
  25. tigrcorn_transports/quic/streams.py +964 -0
  26. tigrcorn_transports/quic/tls_adapter.py +59 -0
  27. tigrcorn_transports/registry.py +10 -0
  28. tigrcorn_transports/tcp/__init__.py +1 -0
  29. tigrcorn_transports/tcp/accept.py +7 -0
  30. tigrcorn_transports/tcp/connection.py +14 -0
  31. tigrcorn_transports/tcp/reader.py +59 -0
  32. tigrcorn_transports/tcp/socketopts.py +13 -0
  33. tigrcorn_transports/tcp/tls.py +5 -0
  34. tigrcorn_transports/tcp/writer.py +8 -0
  35. tigrcorn_transports/udp/__init__.py +3 -0
  36. tigrcorn_transports/udp/endpoint.py +16 -0
  37. tigrcorn_transports/udp/packet.py +14 -0
  38. tigrcorn_transports/udp/socketopts.py +10 -0
  39. tigrcorn_transports/unix/__init__.py +1 -0
  40. tigrcorn_transports/unix/connection.py +10 -0
  41. tigrcorn_transports-0.3.16.dev5.dist-info/METADATA +236 -0
  42. tigrcorn_transports-0.3.16.dev5.dist-info/RECORD +45 -0
  43. tigrcorn_transports-0.3.16.dev5.dist-info/WHEEL +5 -0
  44. tigrcorn_transports-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
  45. tigrcorn_transports-0.3.16.dev5.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ """Transport implementations."""
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class TransportDescriptor:
8
+ name: str
9
+ multiplexed: bool = False
@@ -0,0 +1,3 @@
1
+ from .channel import InProcChannel
2
+
3
+ __all__ = ["InProcChannel"]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class InProcChannel:
9
+ capacity: int = 0
10
+ _queue: asyncio.Queue[bytes] = field(init=False)
11
+
12
+ def __post_init__(self) -> None:
13
+ self._queue = asyncio.Queue(maxsize=self.capacity)
14
+
15
+ async def send(self, payload: bytes) -> None:
16
+ await self._queue.put(payload)
17
+
18
+ async def recv(self) -> bytes:
19
+ return await self._queue.get()
@@ -0,0 +1,4 @@
1
+ from .tcp import TCPListener
2
+ from .unix import UnixListener
3
+
4
+ __all__ = ["TCPListener", "UnixListener"]
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Awaitable, Callable
5
+
6
+
7
+ class BaseListener(ABC):
8
+ @abstractmethod
9
+ async def start(self, client_connected_cb: Callable[..., Awaitable[None]]) -> None:
10
+ raise NotImplementedError
11
+
12
+ @abstractmethod
13
+ async def close(self) -> None:
14
+ raise NotImplementedError
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import Awaitable, Callable
5
+
6
+ from .base import BaseListener
7
+
8
+
9
+ class InProcListener(BaseListener):
10
+ def __init__(self) -> None:
11
+ self._callback: Callable[..., Awaitable[None]] | None = None
12
+
13
+ async def start(self, client_connected_cb: Callable[..., Awaitable[None]]) -> None:
14
+ self._callback = client_connected_cb
15
+
16
+ async def dispatch(self, *args) -> None:
17
+ if self._callback is None:
18
+ raise RuntimeError('in-process listener has not been started')
19
+ result = self._callback(*args)
20
+ if inspect.isawaitable(result):
21
+ await result
22
+
23
+ async def close(self) -> None:
24
+ self._callback = None
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import os
6
+ import stat
7
+ from collections.abc import Awaitable, Callable
8
+ from pathlib import Path
9
+
10
+ from tigrcorn_core.errors import ServerError
11
+ from tigrcorn_transports.pipe.connection import PipeConnection
12
+
13
+ from .base import BaseListener
14
+
15
+
16
+ class PipeListener(BaseListener):
17
+ def __init__(self, path: str) -> None:
18
+ self.path = path
19
+ self._callback: Callable[..., Awaitable[None] | None] | None = None
20
+ self._reader_fd: int | None = None
21
+ self._writer_fd: int | None = None
22
+ self._connection: PipeConnection | None = None
23
+ self._loop: asyncio.AbstractEventLoop | None = None
24
+ self._tasks: set[asyncio.Task[None]] = set()
25
+
26
+ async def start(self, client_connected_cb):
27
+ if not hasattr(os, 'mkfifo'):
28
+ raise ServerError('named pipes are not available on this platform')
29
+ self._callback = client_connected_cb
30
+ self._loop = asyncio.get_running_loop()
31
+ path = Path(self.path)
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ if path.exists():
34
+ mode = path.stat().st_mode
35
+ if not stat.S_ISFIFO(mode):
36
+ raise ServerError(f'{self.path!r} exists and is not a FIFO')
37
+ else:
38
+ os.mkfifo(path)
39
+ self._reader_fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)
40
+ self._writer_fd = os.open(path, os.O_WRONLY | os.O_NONBLOCK)
41
+ self._connection = PipeConnection(path=self.path, read_fd=self._reader_fd, write_fd=self._writer_fd)
42
+ self._loop.add_reader(self._reader_fd, self._on_readable)
43
+
44
+ def _on_readable(self) -> None:
45
+ if self._reader_fd is None or self._callback is None or self._connection is None:
46
+ return
47
+ try:
48
+ data = os.read(self._reader_fd, 65536)
49
+ except BlockingIOError:
50
+ return
51
+ if not data:
52
+ return
53
+ result = self._callback(self._connection, data)
54
+ if inspect.isawaitable(result):
55
+ task = asyncio.create_task(result)
56
+ self._tasks.add(task)
57
+ task.add_done_callback(self._tasks.discard)
58
+
59
+ async def close(self) -> None:
60
+ if self._loop is not None and self._reader_fd is not None:
61
+ self._loop.remove_reader(self._reader_fd)
62
+ for task in list(self._tasks):
63
+ task.cancel()
64
+ for fd in (self._reader_fd, self._writer_fd):
65
+ if fd is not None:
66
+ try:
67
+ os.close(fd)
68
+ except OSError:
69
+ pass
70
+ self._reader_fd = None
71
+ self._writer_fd = None
72
+ self._connection = None
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from tigrcorn_transports.listeners.inproc import InProcListener
4
+ from tigrcorn_transports.listeners.pipe import PipeListener
5
+ from tigrcorn_transports.listeners.tcp import TCPListener
6
+ from tigrcorn_transports.listeners.udp import UDPListener
7
+ from tigrcorn_transports.listeners.unix import UnixListener
8
+
9
+ LISTENER_TYPES = {
10
+ "tcp": TCPListener,
11
+ "udp": UDPListener,
12
+ "unix": UnixListener,
13
+ "pipe": PipeListener,
14
+ "inproc": InProcListener,
15
+ }
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import socket
5
+ from collections.abc import Awaitable, Callable
6
+ from contextlib import suppress
7
+ from typing import Any
8
+
9
+ from tigrcorn_security.tls import wrap_server_tls_connection
10
+ from tigrcorn_transports.tcp.socketopts import configure_socket
11
+
12
+ from .base import BaseListener
13
+
14
+
15
+ class TCPListener(BaseListener):
16
+ def __init__(
17
+ self,
18
+ host: str,
19
+ port: int,
20
+ backlog: int = 2048,
21
+ ssl: Any = None,
22
+ *,
23
+ reuse_port: bool = False,
24
+ reuse_address: bool = True,
25
+ nodelay: bool = True,
26
+ fd: int | None = None,
27
+ sock: socket.socket | None = None,
28
+ ) -> None:
29
+ self.host = host
30
+ self.port = port
31
+ self.backlog = backlog
32
+ self.ssl = ssl
33
+ self.reuse_port = reuse_port
34
+ self.reuse_address = reuse_address
35
+ self.nodelay = nodelay
36
+ self.fd = fd
37
+ self.sock = sock
38
+ self.server: asyncio.AbstractServer | None = None
39
+
40
+ def _get_socket(self) -> socket.socket | None:
41
+ if self.sock is not None:
42
+ return self.sock
43
+ if self.fd is None:
44
+ return None
45
+ sock = socket.socket(fileno=self.fd)
46
+ sock.setblocking(False)
47
+ configure_socket(sock, nodelay=self.nodelay)
48
+ self.sock = sock
49
+ return sock
50
+
51
+ async def start(self, client_connected_cb: Callable[..., Awaitable[None]]) -> None:
52
+ ssl_param = None
53
+ if self.ssl is None:
54
+ callback = client_connected_cb
55
+ elif hasattr(self.ssl, 'certificate_pem') and hasattr(self.ssl, 'private_key_pem'):
56
+ async def callback(raw_reader: asyncio.StreamReader, raw_writer: asyncio.StreamWriter) -> None:
57
+ try:
58
+ connection = await wrap_server_tls_connection(raw_reader, raw_writer, self.ssl)
59
+ except Exception:
60
+ raw_writer.close()
61
+ with suppress(Exception):
62
+ await raw_writer.wait_closed()
63
+ return
64
+ await client_connected_cb(connection, connection)
65
+ else:
66
+ callback = client_connected_cb
67
+ ssl_param = self.ssl
68
+
69
+ existing_sock = self._get_socket()
70
+ if existing_sock is not None:
71
+ self.server = await asyncio.start_server(callback, sock=existing_sock, ssl=ssl_param, backlog=self.backlog)
72
+ else:
73
+ self.server = await asyncio.start_server(
74
+ callback,
75
+ host=self.host,
76
+ port=self.port,
77
+ backlog=self.backlog,
78
+ ssl=ssl_param,
79
+ reuse_port=self.reuse_port,
80
+ reuse_address=self.reuse_address,
81
+ )
82
+ sockets = self.server.sockets or []
83
+ for sock in sockets:
84
+ configure_socket(sock, nodelay=self.nodelay)
85
+
86
+ async def close(self) -> None:
87
+ if self.server is not None:
88
+ self.server.close()
89
+ await self.server.wait_closed()
90
+ self.server = None
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import inspect
5
+ import socket
6
+ from collections.abc import Awaitable, Callable
7
+
8
+ from tigrcorn_transports.udp.endpoint import UDPEndpoint
9
+ from tigrcorn_transports.udp.packet import UDPPacket
10
+ from tigrcorn_transports.udp.socketopts import configure_udp_socket
11
+
12
+ from .base import BaseListener
13
+
14
+
15
+ class _UDPProtocol(asyncio.DatagramProtocol):
16
+ def __init__(self, callback: Callable[..., Awaitable[None] | None]) -> None:
17
+ self.callback = callback
18
+ self.transport: asyncio.DatagramTransport | None = None
19
+ self.endpoint: UDPEndpoint | None = None
20
+ self.tasks: set[asyncio.Task[None]] = set()
21
+
22
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
23
+ self.transport = transport # runtime transport provided by asyncio
24
+ sockname = transport.get_extra_info('sockname')
25
+ sock = transport.get_extra_info('socket')
26
+ if sock is not None:
27
+ configure_udp_socket(sock)
28
+ self.endpoint = UDPEndpoint(transport=transport, local_addr=sockname)
29
+
30
+ def datagram_received(self, data: bytes, addr) -> None: # type: ignore[override]
31
+ if self.endpoint is None:
32
+ return
33
+ packet = UDPPacket(data=data, addr=addr)
34
+ result = self.callback(packet, self.endpoint)
35
+ if inspect.isawaitable(result):
36
+ task = asyncio.create_task(result)
37
+ self.tasks.add(task)
38
+ task.add_done_callback(self.tasks.discard)
39
+
40
+ def connection_lost(self, exc: Exception | None) -> None:
41
+ for task in list(self.tasks):
42
+ task.cancel()
43
+
44
+
45
+ class UDPListener(BaseListener):
46
+ def __init__(self, host: str, port: int, *, reuse_port: bool = False, fd: int | None = None, sock: socket.socket | None = None) -> None:
47
+ self.host = host
48
+ self.port = port
49
+ self.reuse_port = reuse_port
50
+ self.fd = fd
51
+ self.sock = sock
52
+ self.transport: asyncio.DatagramTransport | None = None
53
+ self.protocol: _UDPProtocol | None = None
54
+
55
+ def _get_socket(self) -> socket.socket | None:
56
+ if self.sock is not None:
57
+ return self.sock
58
+ if self.fd is None:
59
+ return None
60
+ sock = socket.socket(fileno=self.fd)
61
+ sock.setblocking(False)
62
+ configure_udp_socket(sock)
63
+ self.sock = sock
64
+ return sock
65
+
66
+ async def start(self, client_connected_cb):
67
+ loop = asyncio.get_running_loop()
68
+ existing_sock = self._get_socket()
69
+ transport, protocol = await loop.create_datagram_endpoint(
70
+ lambda: _UDPProtocol(client_connected_cb),
71
+ local_addr=None if existing_sock is not None else (self.host, self.port),
72
+ reuse_port=self.reuse_port if existing_sock is None else None,
73
+ sock=existing_sock,
74
+ )
75
+ self.transport = transport
76
+ self.protocol = protocol
77
+
78
+ async def close(self) -> None:
79
+ if self.transport is not None:
80
+ self.transport.close()
81
+ self.transport = None
82
+ self.protocol = None
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import socket
5
+ from collections.abc import Awaitable, Callable
6
+ from contextlib import suppress
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from tigrcorn_security.tls import wrap_server_tls_connection
11
+ from tigrcorn_core.utils.net import ensure_parent_dir
12
+
13
+ from .base import BaseListener
14
+
15
+
16
+ class UnixListener(BaseListener):
17
+ def __init__(self, path: str, backlog: int = 2048, ssl: Any = None, *, fd: int | None = None, sock: socket.socket | None = None) -> None:
18
+ self.path = path
19
+ self.backlog = backlog
20
+ self.ssl = ssl
21
+ self.fd = fd
22
+ self.sock = sock
23
+ self.server: asyncio.AbstractServer | None = None
24
+
25
+ def _get_socket(self) -> socket.socket | None:
26
+ if self.sock is not None:
27
+ return self.sock
28
+ if self.fd is None:
29
+ return None
30
+ sock = socket.socket(fileno=self.fd)
31
+ sock.setblocking(False)
32
+ self.sock = sock
33
+ return sock
34
+
35
+ async def start(self, client_connected_cb: Callable[..., Awaitable[None]]) -> None:
36
+ path = Path(self.path) if self.path else None
37
+ existing_sock = self._get_socket()
38
+ if existing_sock is None and path is not None:
39
+ ensure_parent_dir(str(path))
40
+ if path.exists():
41
+ path.unlink()
42
+
43
+ ssl_param = None
44
+ if self.ssl is None:
45
+ callback = client_connected_cb
46
+ elif hasattr(self.ssl, 'certificate_pem') and hasattr(self.ssl, 'private_key_pem'):
47
+ async def callback(raw_reader: asyncio.StreamReader, raw_writer: asyncio.StreamWriter) -> None:
48
+ try:
49
+ connection = await wrap_server_tls_connection(raw_reader, raw_writer, self.ssl)
50
+ except Exception:
51
+ raw_writer.close()
52
+ with suppress(Exception):
53
+ await raw_writer.wait_closed()
54
+ return
55
+ await client_connected_cb(connection, connection)
56
+ else:
57
+ callback = client_connected_cb
58
+ ssl_param = self.ssl
59
+
60
+ if existing_sock is not None:
61
+ self.server = await asyncio.start_unix_server(callback, sock=existing_sock, ssl=ssl_param, backlog=self.backlog)
62
+ else:
63
+ self.server = await asyncio.start_unix_server(callback, path=self.path, backlog=self.backlog, ssl=ssl_param)
64
+
65
+ async def close(self) -> None:
66
+ if self.server is not None:
67
+ self.server.close()
68
+ await self.server.wait_closed()
69
+ self.server = None
@@ -0,0 +1,3 @@
1
+ from .connection import PipeConnection
2
+
3
+ __all__ = ["PipeConnection"]
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(slots=True)
8
+ class PipeConnection:
9
+ path: str
10
+ read_fd: int
11
+ write_fd: int | None = None
12
+
13
+ def read(self, n: int = 65536) -> bytes:
14
+ return os.read(self.read_fd, n)
15
+
16
+ def write(self, data: bytes) -> int:
17
+ if self.write_fd is None:
18
+ raise OSError('pipe is not writable')
19
+ return os.write(self.write_fd, data)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,108 @@
1
+ from .crypto import (
2
+ QUIC_V1_INITIAL_SALT,
3
+ QuicPacketProtectionKeys,
4
+ aes_gcm_decrypt,
5
+ aes_gcm_encrypt,
6
+ apply_header_protection,
7
+ derive_initial_packet_protection_keys,
8
+ derive_initial_secret,
9
+ derive_quic_packet_protection_keys,
10
+ derive_secret,
11
+ generate_connection_id,
12
+ hkdf_expand_label,
13
+ hkdf_extract,
14
+ make_integrity_tag,
15
+ packet_nonce,
16
+ protect_payload,
17
+ remove_header_protection,
18
+ unprotect_payload,
19
+ unprotect_quic_packet,
20
+ protect_quic_packet,
21
+ )
22
+ from .datagrams import QuicDatagram, QuicHeader, QuicPacketType, decode_datagram, encode_datagram
23
+ from .packets import (
24
+ QuicLongHeaderPacket,
25
+ QuicLongHeaderType,
26
+ QuicRetryPacket,
27
+ QuicShortHeaderPacket,
28
+ QuicStatelessResetPacket,
29
+ QuicVersionNegotiationPacket,
30
+ decode_packet,
31
+ decode_long_header_packet,
32
+ decode_short_header_packet,
33
+ parse_stateless_reset,
34
+ )
35
+ from .streams import QuicStreamFrame, QuicAckFrame, QuicConnectionCloseFrame, QuicMaxDataFrame, QuicMaxStreamDataFrame
36
+
37
+ __all__ = [
38
+ 'QuicConnection',
39
+ 'QuicEvent',
40
+ 'QuicDatagram',
41
+ 'QuicHeader',
42
+ 'QuicPacketType',
43
+ 'QuicStreamFrame',
44
+ 'QuicAckFrame',
45
+ 'QuicConnectionCloseFrame',
46
+ 'QuicMaxDataFrame',
47
+ 'QuicMaxStreamDataFrame',
48
+ 'decode_datagram',
49
+ 'encode_datagram',
50
+ 'QuicLongHeaderPacket',
51
+ 'QuicLongHeaderType',
52
+ 'QuicRetryPacket',
53
+ 'QuicShortHeaderPacket',
54
+ 'QuicStatelessResetPacket',
55
+ 'QuicVersionNegotiationPacket',
56
+ 'decode_packet',
57
+ 'decode_long_header_packet',
58
+ 'decode_short_header_packet',
59
+ 'parse_stateless_reset',
60
+ 'QUIC_V1_INITIAL_SALT',
61
+ 'QuicPacketProtectionKeys',
62
+ 'aes_gcm_encrypt',
63
+ 'aes_gcm_decrypt',
64
+ 'apply_header_protection',
65
+ 'remove_header_protection',
66
+ 'protect_quic_packet',
67
+ 'unprotect_quic_packet',
68
+ 'derive_initial_secret',
69
+ 'derive_initial_packet_protection_keys',
70
+ 'derive_quic_packet_protection_keys',
71
+ 'hkdf_extract',
72
+ 'hkdf_expand_label',
73
+ 'packet_nonce',
74
+ 'derive_secret',
75
+ 'generate_connection_id',
76
+ 'make_integrity_tag',
77
+ 'protect_payload',
78
+ 'unprotect_payload',
79
+ 'QuicTlsHandshakeDriver',
80
+ 'TransportParameters',
81
+ 'generate_self_signed_certificate',
82
+ 'QuicLossRecovery',
83
+ ]
84
+
85
+
86
+ def __getattr__(name: str):
87
+ if name in {"QuicConnection", "QuicEvent"}:
88
+ from .connection import QuicConnection, QuicEvent
89
+
90
+ mapping = {
91
+ "QuicConnection": QuicConnection,
92
+ "QuicEvent": QuicEvent,
93
+ }
94
+ return mapping[name]
95
+ if name in {"QuicTlsHandshakeDriver", "TransportParameters", "generate_self_signed_certificate"}:
96
+ from .handshake import QuicTlsHandshakeDriver, TransportParameters, generate_self_signed_certificate
97
+
98
+ mapping = {
99
+ "QuicTlsHandshakeDriver": QuicTlsHandshakeDriver,
100
+ "TransportParameters": TransportParameters,
101
+ "generate_self_signed_certificate": generate_self_signed_certificate,
102
+ }
103
+ return mapping[name]
104
+ if name == "QuicLossRecovery":
105
+ from .recovery import QuicLossRecovery
106
+
107
+ return QuicLossRecovery
108
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")