Pyjoyment 0.1.0__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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/IOLoop/Client.py
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.IOLoop.Client - Non-blocking TCP/IP and UNIX domain socket client
|
|
3
|
+
======================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.IOLoop.Client import Client
|
|
7
|
+
|
|
8
|
+
# Create socket connection
|
|
9
|
+
client = Client()
|
|
10
|
+
client.on('connect', lambda client, handle: ...)
|
|
11
|
+
client.on('error', lambda client, err: ...)
|
|
12
|
+
client.connect(address='example.com', port=80)
|
|
13
|
+
|
|
14
|
+
# Or import the module, as in Google Python Style Guide
|
|
15
|
+
|
|
16
|
+
import Pyjo.IOLoop.Client
|
|
17
|
+
|
|
18
|
+
# Create socket connection
|
|
19
|
+
client = Pyjo.IOLoop.Client.new()
|
|
20
|
+
client.on('connect', lambda client, handle: ...)
|
|
21
|
+
client.on('error', lambda client, err: ...)
|
|
22
|
+
client.connect(address='example.com', port=80)
|
|
23
|
+
|
|
24
|
+
# Start reactor if necessary
|
|
25
|
+
if not client.reactor.is_running:
|
|
26
|
+
client.reactor.start()
|
|
27
|
+
|
|
28
|
+
:mod:`Pyjo.IOLoop.Client` opens TCP/IP and UNIX domain socket connections for
|
|
29
|
+
:mod:`Pyjo.IOLoop`.
|
|
30
|
+
|
|
31
|
+
The end of a ``with`` block closes the connection.
|
|
32
|
+
|
|
33
|
+
Events
|
|
34
|
+
------
|
|
35
|
+
|
|
36
|
+
:mod:`Pyjo.IOLoop.Client` inherits all events from :mod:`Pyjo.EventEmitter`
|
|
37
|
+
and can emit the following new ones.
|
|
38
|
+
|
|
39
|
+
connect
|
|
40
|
+
~~~~~~~
|
|
41
|
+
::
|
|
42
|
+
|
|
43
|
+
client.on('connect', lambda client, handle: ...)
|
|
44
|
+
|
|
45
|
+
Emitted once the connection is established, with its :class:`socket.socket`,
|
|
46
|
+
or the :class:`ssl.SSLSocket` once TLS has been negotiated.
|
|
47
|
+
|
|
48
|
+
error
|
|
49
|
+
~~~~~
|
|
50
|
+
::
|
|
51
|
+
|
|
52
|
+
client.on('error', lambda client, err: ...)
|
|
53
|
+
|
|
54
|
+
Emitted if an error occurs on the connection, fatal if unhandled. The error is
|
|
55
|
+
a message, or the :class:`OSError` of the system.
|
|
56
|
+
|
|
57
|
+
See also
|
|
58
|
+
--------
|
|
59
|
+
|
|
60
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
61
|
+
|
|
62
|
+
Ported from `Mojo::IOLoop::Client <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/IOLoop/Client.pm>`_
|
|
63
|
+
of Mojolicious 9.49.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
import Pyjo.EventEmitter
|
|
67
|
+
import Pyjo.IOLoop
|
|
68
|
+
import Pyjo.IOLoop.TLS
|
|
69
|
+
import Pyjo.Reactor
|
|
70
|
+
|
|
71
|
+
import concurrent.futures
|
|
72
|
+
import errno
|
|
73
|
+
import os
|
|
74
|
+
import socket
|
|
75
|
+
import sys
|
|
76
|
+
import threading
|
|
77
|
+
import time
|
|
78
|
+
import weakref
|
|
79
|
+
|
|
80
|
+
from collections.abc import Callable, Generator
|
|
81
|
+
from types import TracebackType
|
|
82
|
+
from typing import Any, Literal, Self
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Non-blocking name resolution, which Mojo does with Net::DNS::Native, is a thread of the standard library here
|
|
86
|
+
NNR = not os.environ.get('PYJO_NO_NNR')
|
|
87
|
+
_executor: concurrent.futures.ThreadPoolExecutor | None = None
|
|
88
|
+
|
|
89
|
+
# SOCKS5 support, which Mojo does with IO::Socket::Socks, is a client of its own here
|
|
90
|
+
SOCKS = not os.environ.get('PYJO_NO_SOCKS')
|
|
91
|
+
|
|
92
|
+
# The replies of a SOCKS5 server, with the messages of IO::Socket::Socks
|
|
93
|
+
_SOCKS_REPLIES = {
|
|
94
|
+
1: 'General failure',
|
|
95
|
+
2: 'Not allowed',
|
|
96
|
+
3: 'Network unreachable',
|
|
97
|
+
4: 'Host unreachable',
|
|
98
|
+
5: 'Connection refused',
|
|
99
|
+
6: 'TTL expired',
|
|
100
|
+
7: 'Command not supported',
|
|
101
|
+
8: 'Address not supported',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type AddrInfo = list[tuple[Any, Any, Any, Any, Any]]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# The threads of the pool are stopped before a fork, as a process with threads may deadlock in its child, which Python
|
|
108
|
+
# warns about, and the child would have a pool without its threads; the next resolution starts the pool again
|
|
109
|
+
def _before_fork() -> None:
|
|
110
|
+
global _executor
|
|
111
|
+
if (executor := _executor) is not None:
|
|
112
|
+
_executor = None
|
|
113
|
+
executor.shutdown(wait=True)
|
|
114
|
+
|
|
115
|
+
# Linux lists a thread that has been joined for a moment more, which Python 3.15 counts as a thread when it
|
|
116
|
+
# forks, so the fork waits a little for the threads of the system to be those of Python
|
|
117
|
+
tasks = '/proc/self/task'
|
|
118
|
+
deadline = time.monotonic() + 0.1
|
|
119
|
+
while (
|
|
120
|
+
os.path.isdir(tasks) and len(os.listdir(tasks)) > threading.active_count() and time.monotonic() < deadline
|
|
121
|
+
):
|
|
122
|
+
time.sleep(0.001)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if hasattr(os, 'register_at_fork'):
|
|
126
|
+
os.register_at_fork(before=_before_fork)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _Resolver:
|
|
130
|
+
"""A name resolution in a thread, which makes a socket readable once it is done, for Pyjo.IOLoop.Datagram too."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, host: str, port: int, type: int = socket.SOCK_STREAM, family: int = 0) -> None:
|
|
133
|
+
global _executor
|
|
134
|
+
if _executor is None:
|
|
135
|
+
_executor = concurrent.futures.ThreadPoolExecutor(max_workers=5, thread_name_prefix='Pyjo.IOLoop.Client')
|
|
136
|
+
self.handle, self._writer = socket.socketpair()
|
|
137
|
+
self.handle.setblocking(False)
|
|
138
|
+
self.future = _executor.submit(
|
|
139
|
+
socket.getaddrinfo,
|
|
140
|
+
host,
|
|
141
|
+
port,
|
|
142
|
+
family,
|
|
143
|
+
type,
|
|
144
|
+
socket.IPPROTO_TCP if type == socket.SOCK_STREAM else socket.IPPROTO_UDP,
|
|
145
|
+
)
|
|
146
|
+
self.future.add_done_callback(self._done)
|
|
147
|
+
|
|
148
|
+
def _done(self, future: Any) -> None:
|
|
149
|
+
try:
|
|
150
|
+
self._writer.send(b'.')
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def close(self) -> None:
|
|
155
|
+
self.future.cancel()
|
|
156
|
+
self.handle.close()
|
|
157
|
+
self._writer.close()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class _SocksError(Exception):
|
|
161
|
+
"""An error of the SOCKS5 handshake, with the message of IO::Socket::Socks."""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class _Socks:
|
|
165
|
+
"""
|
|
166
|
+
A SOCKS5 handshake on a socket that does not block, as the client of
|
|
167
|
+
IO::Socket::Socks does it: the proxy resolves the name, and the user and
|
|
168
|
+
the password of RFC 1929 are offered next to no authentication.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, handle: socket.socket, address: str, port: int, user: str | None, password: str | None) -> None:
|
|
172
|
+
self.handle = handle
|
|
173
|
+
self._steps = self._handshake(address, port, user, password)
|
|
174
|
+
self._done = False
|
|
175
|
+
|
|
176
|
+
def step(self) -> Literal['read', 'write'] | None:
|
|
177
|
+
"""What the handshake waits for, or ``None`` once it is done."""
|
|
178
|
+
if self._done:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
return next(self._steps)
|
|
182
|
+
except StopIteration:
|
|
183
|
+
self._done = True
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def _handshake(
|
|
187
|
+
self, address: str, port: int, user: str | None, password: str | None
|
|
188
|
+
) -> Generator[Literal['read', 'write']]:
|
|
189
|
+
# The auth mechanisms
|
|
190
|
+
methods = b'\x00\x02' if user else b'\x00'
|
|
191
|
+
yield from self._send(b'\x05' + bytes([len(methods)]) + methods)
|
|
192
|
+
_, method = yield from self._read(2)
|
|
193
|
+
if method == 0xFF:
|
|
194
|
+
raise _SocksError('No valid auth mechanisms')
|
|
195
|
+
|
|
196
|
+
# The user and the password
|
|
197
|
+
if method != 0x00:
|
|
198
|
+
name, secret = (user or '').encode(), (password or '').encode()
|
|
199
|
+
yield from self._send(b'\x01' + bytes([len(name)]) + name + bytes([len(secret)]) + secret)
|
|
200
|
+
_, status = yield from self._read(2)
|
|
201
|
+
if status != 0x00:
|
|
202
|
+
raise _SocksError('Authentication failed with SOCKS5 proxy')
|
|
203
|
+
|
|
204
|
+
# The command, with the name that the proxy resolves
|
|
205
|
+
host = _encode_host(address)
|
|
206
|
+
yield from self._send(b'\x05\x01\x00\x03' + bytes([len(host)]) + host + port.to_bytes(2, 'big'))
|
|
207
|
+
|
|
208
|
+
# The reply
|
|
209
|
+
_, reply, _, atyp = yield from self._read(4)
|
|
210
|
+
if atyp == 0x03:
|
|
211
|
+
(length,) = yield from self._read(1)
|
|
212
|
+
yield from self._read(length)
|
|
213
|
+
elif atyp == 0x01:
|
|
214
|
+
yield from self._read(4)
|
|
215
|
+
elif atyp == 0x04:
|
|
216
|
+
yield from self._read(16)
|
|
217
|
+
else:
|
|
218
|
+
raise _SocksError(f'Unsupported address type returned by socks server: {atyp}')
|
|
219
|
+
yield from self._read(2)
|
|
220
|
+
if reply != 0x00:
|
|
221
|
+
raise _SocksError(_SOCKS_REPLIES.get(reply, _SOCKS_REPLIES[1]))
|
|
222
|
+
|
|
223
|
+
def _read(self, length: int) -> Generator[Literal['read', 'write'], None, bytes]:
|
|
224
|
+
data = b''
|
|
225
|
+
while len(data) < length:
|
|
226
|
+
try:
|
|
227
|
+
chunk = self.handle.recv(length - len(data))
|
|
228
|
+
except (BlockingIOError, InterruptedError):
|
|
229
|
+
yield 'read'
|
|
230
|
+
continue
|
|
231
|
+
except OSError as e:
|
|
232
|
+
raise _SocksError(f'read: {e.strerror or e}') from e
|
|
233
|
+
if not len(chunk):
|
|
234
|
+
raise _SocksError('Socket closed by remote side')
|
|
235
|
+
data += chunk
|
|
236
|
+
return data
|
|
237
|
+
|
|
238
|
+
def _send(self, data: bytes) -> Generator[Literal['read', 'write']]:
|
|
239
|
+
while len(data):
|
|
240
|
+
try:
|
|
241
|
+
data = data[self.handle.send(data) :]
|
|
242
|
+
except (BlockingIOError, InterruptedError):
|
|
243
|
+
yield 'write'
|
|
244
|
+
except OSError as e:
|
|
245
|
+
raise _SocksError(f'send: {e.strerror or e}') from e
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Pyjo_IOLoop_Client(Pyjo.EventEmitter.object):
|
|
249
|
+
"""
|
|
250
|
+
:mod:`Pyjo.IOLoop.Client` inherits all methods from
|
|
251
|
+
:mod:`Pyjo.EventEmitter` and implements the following new ones.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, *, reactor: Pyjo.Reactor.object | None = None) -> None:
|
|
255
|
+
"""::
|
|
256
|
+
|
|
257
|
+
client = Pyjo.IOLoop.Client.new()
|
|
258
|
+
client = Pyjo.IOLoop.Client.new(reactor=Pyjo.Reactor.Poll.new())
|
|
259
|
+
|
|
260
|
+
Construct a new :mod:`Pyjo.IOLoop.Client` object.
|
|
261
|
+
"""
|
|
262
|
+
super().__init__()
|
|
263
|
+
self._addresses: AddrInfo = []
|
|
264
|
+
self._dns: _Resolver | None = None
|
|
265
|
+
self._socks_handshake: _Socks | None = None
|
|
266
|
+
self._handle: Any = None
|
|
267
|
+
self._reactor: weakref.ref[Pyjo.Reactor.object] | None = None
|
|
268
|
+
self._timer: str | None = None
|
|
269
|
+
if reactor is not None:
|
|
270
|
+
self.reactor = reactor
|
|
271
|
+
|
|
272
|
+
def __del__(self) -> None:
|
|
273
|
+
# Module globals may be gone at the end of the interpreter
|
|
274
|
+
try:
|
|
275
|
+
if not sys.is_finalizing():
|
|
276
|
+
reactor = self._reactor() if self._reactor is not None else None
|
|
277
|
+
Pyjo.Reactor._finalize(self.close, reactor) # pyright: ignore[reportPrivateUsage]
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
# Not in Mojolicious: a context manager, which closes the connection where the block ends, as PyPy frees the object
|
|
282
|
+
# only when its garbage collector runs
|
|
283
|
+
def __enter__(self) -> Self:
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def __exit__(
|
|
287
|
+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
|
288
|
+
) -> None:
|
|
289
|
+
self.close()
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def reactor(self) -> Pyjo.Reactor.object:
|
|
293
|
+
"""::
|
|
294
|
+
|
|
295
|
+
reactor = client.reactor
|
|
296
|
+
client.reactor = Pyjo.Reactor.Poll.new()
|
|
297
|
+
|
|
298
|
+
Low-level event reactor, defaults to the ``reactor`` attribute value of
|
|
299
|
+
the global :mod:`Pyjo.IOLoop` singleton. Note that this attribute is
|
|
300
|
+
weakened.
|
|
301
|
+
"""
|
|
302
|
+
if self._reactor is None:
|
|
303
|
+
self._reactor = weakref.ref(Pyjo.IOLoop.singleton.reactor)
|
|
304
|
+
if (reactor := self._reactor()) is None:
|
|
305
|
+
raise ReferenceError('weakly-referenced object no longer exists')
|
|
306
|
+
return reactor
|
|
307
|
+
|
|
308
|
+
@reactor.setter
|
|
309
|
+
def reactor(self, value: Pyjo.Reactor.object) -> None:
|
|
310
|
+
self._reactor = weakref.ref(value)
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def can_nnr(cls) -> bool:
|
|
314
|
+
"""::
|
|
315
|
+
|
|
316
|
+
boolean = Pyjo.IOLoop.Client.object.can_nnr()
|
|
317
|
+
|
|
318
|
+
True if non-blocking name resolution support is enabled, as it is
|
|
319
|
+
unless the ``PYJO_NO_NNR`` environment variable is set. The names are
|
|
320
|
+
resolved in a thread.
|
|
321
|
+
"""
|
|
322
|
+
return NNR
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def can_socks(cls) -> bool:
|
|
326
|
+
"""::
|
|
327
|
+
|
|
328
|
+
boolean = Pyjo.IOLoop.Client.object.can_socks()
|
|
329
|
+
|
|
330
|
+
True if SOCKS5 support is enabled, as it is unless the
|
|
331
|
+
``PYJO_NO_SOCKS`` environment variable is set.
|
|
332
|
+
"""
|
|
333
|
+
return SOCKS
|
|
334
|
+
|
|
335
|
+
# Not in Mojolicious: what DESTROY does, which Python does when it frees the client, PyPy only later
|
|
336
|
+
def close(self) -> None:
|
|
337
|
+
"""::
|
|
338
|
+
|
|
339
|
+
client.close()
|
|
340
|
+
|
|
341
|
+
Stop connecting and close a socket that is not connected yet, as it
|
|
342
|
+
happens when the client is freed.
|
|
343
|
+
"""
|
|
344
|
+
handle = self._handle
|
|
345
|
+
self._cleanup()
|
|
346
|
+
if handle is not None:
|
|
347
|
+
handle.close()
|
|
348
|
+
|
|
349
|
+
def connect(self, **options: Any) -> None:
|
|
350
|
+
"""::
|
|
351
|
+
|
|
352
|
+
client.connect(address='127.0.0.1', port=3000)
|
|
353
|
+
|
|
354
|
+
Open a socket connection to a remote host.
|
|
355
|
+
|
|
356
|
+
These options are currently available:
|
|
357
|
+
|
|
358
|
+
address
|
|
359
|
+
::
|
|
360
|
+
|
|
361
|
+
address='mojolicious.org'
|
|
362
|
+
|
|
363
|
+
Address or host name of the peer to connect to, defaults to
|
|
364
|
+
``127.0.0.1``.
|
|
365
|
+
|
|
366
|
+
handle
|
|
367
|
+
::
|
|
368
|
+
|
|
369
|
+
handle=handle
|
|
370
|
+
|
|
371
|
+
Use an already prepared :class:`socket.socket` object.
|
|
372
|
+
|
|
373
|
+
path
|
|
374
|
+
::
|
|
375
|
+
|
|
376
|
+
path='/tmp/myapp.sock'
|
|
377
|
+
|
|
378
|
+
Path of UNIX domain socket to connect to.
|
|
379
|
+
|
|
380
|
+
port
|
|
381
|
+
::
|
|
382
|
+
|
|
383
|
+
port=80
|
|
384
|
+
|
|
385
|
+
Port to connect to, defaults to ``80`` or ``443`` with ``tls``
|
|
386
|
+
option.
|
|
387
|
+
|
|
388
|
+
source_address
|
|
389
|
+
::
|
|
390
|
+
|
|
391
|
+
source_address=('127.0.0.1', 0)
|
|
392
|
+
|
|
393
|
+
Local address and port to connect from.
|
|
394
|
+
|
|
395
|
+
socks_address
|
|
396
|
+
::
|
|
397
|
+
|
|
398
|
+
socks_address='127.0.0.1'
|
|
399
|
+
|
|
400
|
+
Address or host name of SOCKS5 proxy server to use for connection.
|
|
401
|
+
|
|
402
|
+
socks_pass
|
|
403
|
+
::
|
|
404
|
+
|
|
405
|
+
socks_pass='secr3t'
|
|
406
|
+
|
|
407
|
+
Password to use for SOCKS5 authentication.
|
|
408
|
+
|
|
409
|
+
socks_port
|
|
410
|
+
::
|
|
411
|
+
|
|
412
|
+
socks_port=9050
|
|
413
|
+
|
|
414
|
+
Port of SOCKS5 proxy server to use for connection.
|
|
415
|
+
|
|
416
|
+
socks_user
|
|
417
|
+
::
|
|
418
|
+
|
|
419
|
+
socks_user='sri'
|
|
420
|
+
|
|
421
|
+
Username to use for SOCKS5 authentication.
|
|
422
|
+
|
|
423
|
+
timeout
|
|
424
|
+
::
|
|
425
|
+
|
|
426
|
+
timeout=15
|
|
427
|
+
|
|
428
|
+
Maximum amount of time in seconds establishing connection may take
|
|
429
|
+
before getting canceled, defaults to ``10``.
|
|
430
|
+
|
|
431
|
+
tls
|
|
432
|
+
::
|
|
433
|
+
|
|
434
|
+
tls=True
|
|
435
|
+
|
|
436
|
+
Enable TLS.
|
|
437
|
+
|
|
438
|
+
tls_ca
|
|
439
|
+
::
|
|
440
|
+
|
|
441
|
+
tls_ca='/etc/tls/ca.crt'
|
|
442
|
+
|
|
443
|
+
Path to TLS certificate authority file.
|
|
444
|
+
|
|
445
|
+
tls_cert
|
|
446
|
+
::
|
|
447
|
+
|
|
448
|
+
tls_cert='/etc/tls/client.crt'
|
|
449
|
+
|
|
450
|
+
Path to the TLS certificate file.
|
|
451
|
+
|
|
452
|
+
tls_context
|
|
453
|
+
::
|
|
454
|
+
|
|
455
|
+
tls_context=ssl.create_default_context()
|
|
456
|
+
|
|
457
|
+
The :class:`ssl.SSLContext` to use instead of the one that the
|
|
458
|
+
other options make.
|
|
459
|
+
|
|
460
|
+
tls_hostname
|
|
461
|
+
::
|
|
462
|
+
|
|
463
|
+
tls_hostname='mojolicious.org'
|
|
464
|
+
|
|
465
|
+
Name of the server for SNI and the verification of its
|
|
466
|
+
certificate instead of ``address``, an empty one for no SNI.
|
|
467
|
+
|
|
468
|
+
tls_key
|
|
469
|
+
::
|
|
470
|
+
|
|
471
|
+
tls_key='/etc/tls/client.key'
|
|
472
|
+
|
|
473
|
+
Path to the TLS key file.
|
|
474
|
+
"""
|
|
475
|
+
args = dict(options)
|
|
476
|
+
|
|
477
|
+
# Timeout
|
|
478
|
+
ref = weakref.ref(self)
|
|
479
|
+
reactor = self.reactor
|
|
480
|
+
|
|
481
|
+
def timeout(reactor: Any) -> None:
|
|
482
|
+
if (client := ref()) is not None:
|
|
483
|
+
client.emit('error', 'Connect timeout')
|
|
484
|
+
|
|
485
|
+
self._timer = reactor.timer(args.get('timeout') or 10, timeout)
|
|
486
|
+
|
|
487
|
+
# Blocking name resolution
|
|
488
|
+
for key in ('address', 'socks_address'):
|
|
489
|
+
if args.get(key):
|
|
490
|
+
args[key] = str(args[key]).replace('[', '').replace(']', '')
|
|
491
|
+
if not args.get('address'):
|
|
492
|
+
args['address'] = '127.0.0.1'
|
|
493
|
+
address = args.get('socks_address') or args['address']
|
|
494
|
+
if not NNR or args.get('handle') or args.get('path'):
|
|
495
|
+
|
|
496
|
+
def connect(reactor: Any) -> None:
|
|
497
|
+
if (client := ref()) is not None:
|
|
498
|
+
client._connect(args)
|
|
499
|
+
|
|
500
|
+
return reactor.next_tick(connect)
|
|
501
|
+
|
|
502
|
+
# Non-blocking name resolution
|
|
503
|
+
dns = self._dns = _Resolver(address, _port(args))
|
|
504
|
+
|
|
505
|
+
def resolved(reactor: Any, writable: bool) -> None:
|
|
506
|
+
if (client := ref()) is None:
|
|
507
|
+
return
|
|
508
|
+
reactor.remove(dns.handle)
|
|
509
|
+
client._dns = None
|
|
510
|
+
try:
|
|
511
|
+
args['addr_info'] = dns.future.result()
|
|
512
|
+
except OSError as e:
|
|
513
|
+
client.emit('error', f"Can't resolve: {e.strerror or e}")
|
|
514
|
+
return
|
|
515
|
+
finally:
|
|
516
|
+
dns.close()
|
|
517
|
+
client._connect(args)
|
|
518
|
+
|
|
519
|
+
reactor.io(dns.handle, resolved).watch(dns.handle, True, False)
|
|
520
|
+
|
|
521
|
+
def _cleanup(self) -> None:
|
|
522
|
+
reactor = self._reactor() if self._reactor is not None else None
|
|
523
|
+
if (dns := self._dns) is not None:
|
|
524
|
+
self._dns = None
|
|
525
|
+
# The reactor forgets the socket before it is closed, as epoll does and asyncio does not
|
|
526
|
+
if reactor is not None:
|
|
527
|
+
try:
|
|
528
|
+
reactor.remove(dns.handle)
|
|
529
|
+
except Exception:
|
|
530
|
+
pass
|
|
531
|
+
dns.close()
|
|
532
|
+
if reactor is None:
|
|
533
|
+
return
|
|
534
|
+
for name in ('_timer', '_handle'):
|
|
535
|
+
if (value := getattr(self, name)) is not None:
|
|
536
|
+
setattr(self, name, None)
|
|
537
|
+
try:
|
|
538
|
+
reactor.remove(value)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
def _connect(self, args: dict[str, Any]) -> None:
|
|
543
|
+
path = args.get('path')
|
|
544
|
+
handle = self._handle = args.get('handle')
|
|
545
|
+
|
|
546
|
+
if handle is None:
|
|
547
|
+
try:
|
|
548
|
+
# UNIX domain socket
|
|
549
|
+
if path:
|
|
550
|
+
# Not in Mojolicious: Python has no UNIX domain sockets on Windows
|
|
551
|
+
if sys.platform == 'win32':
|
|
552
|
+
raise OSError(errno.EAFNOSUPPORT, 'UNIX domain sockets are not supported')
|
|
553
|
+
handle = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
554
|
+
handle.setblocking(False)
|
|
555
|
+
_connect_ex(handle, path)
|
|
556
|
+
|
|
557
|
+
# IP socket, as IO::Socket::IP tries the addresses of the name one after another
|
|
558
|
+
else:
|
|
559
|
+
self._addresses = list(
|
|
560
|
+
args.get('addr_info')
|
|
561
|
+
or socket.getaddrinfo(
|
|
562
|
+
args.get('socks_address') or args['address'],
|
|
563
|
+
_port(args),
|
|
564
|
+
type=socket.SOCK_STREAM,
|
|
565
|
+
proto=socket.IPPROTO_TCP,
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
handle = self._next(args)
|
|
569
|
+
except OSError as e:
|
|
570
|
+
self.emit('error', f"Can't connect: {e.strerror or e}")
|
|
571
|
+
return
|
|
572
|
+
self._handle = handle
|
|
573
|
+
handle.setblocking(False)
|
|
574
|
+
|
|
575
|
+
if path:
|
|
576
|
+
self._try_socks(args)
|
|
577
|
+
else:
|
|
578
|
+
self._wait(self._ready, handle, args)
|
|
579
|
+
|
|
580
|
+
# The next address of the name that a socket can connect from, as IO::Socket::IP tries them all
|
|
581
|
+
def _next(self, args: dict[str, Any]) -> socket.socket:
|
|
582
|
+
error: OSError | None = None
|
|
583
|
+
while len(self._addresses):
|
|
584
|
+
family, type, proto, _, sockaddr = self._addresses.pop(0)
|
|
585
|
+
handle = socket.socket(family, type, proto)
|
|
586
|
+
try:
|
|
587
|
+
handle.setblocking(False)
|
|
588
|
+
# Unlike Mojolicious: socket_options of IO::Socket::IP is source_address, as socket.create_connection has
|
|
589
|
+
# it
|
|
590
|
+
if (source := args.get('source_address')) is not None:
|
|
591
|
+
handle.bind(source)
|
|
592
|
+
_connect_ex(handle, sockaddr)
|
|
593
|
+
except OSError as e:
|
|
594
|
+
handle.close()
|
|
595
|
+
error = e
|
|
596
|
+
continue
|
|
597
|
+
return handle
|
|
598
|
+
raise error or OSError(errno.EADDRNOTAVAIL, os.strerror(errno.EADDRNOTAVAIL))
|
|
599
|
+
|
|
600
|
+
def _ready(self, args: dict[str, Any]) -> None:
|
|
601
|
+
handle = self._handle
|
|
602
|
+
if handle is None:
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
# Socket changes in between attempts and needs to be re-added for epoll/kqueue
|
|
606
|
+
if err := handle.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR):
|
|
607
|
+
self.reactor.remove(handle)
|
|
608
|
+
handle.close()
|
|
609
|
+
self._handle = None
|
|
610
|
+
if not len(self._addresses):
|
|
611
|
+
self.emit('error', OSError(err, os.strerror(err)))
|
|
612
|
+
return
|
|
613
|
+
try:
|
|
614
|
+
handle = self._handle = self._next(args)
|
|
615
|
+
except OSError as e:
|
|
616
|
+
self.emit('error', e)
|
|
617
|
+
return
|
|
618
|
+
return self._wait(self._ready, handle, args)
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
handle.getpeername()
|
|
622
|
+
except OSError as e:
|
|
623
|
+
self.emit('error', e)
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
# Disable Nagle's algorithm
|
|
627
|
+
try:
|
|
628
|
+
handle.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
629
|
+
except OSError:
|
|
630
|
+
pass
|
|
631
|
+
|
|
632
|
+
self._try_socks(args)
|
|
633
|
+
|
|
634
|
+
def _socks(self, args: dict[str, Any]) -> None:
|
|
635
|
+
handle, socks = self._handle, self._socks_handshake
|
|
636
|
+
if handle is None or socks is None:
|
|
637
|
+
return
|
|
638
|
+
try:
|
|
639
|
+
want = socks.step()
|
|
640
|
+
except _SocksError as e:
|
|
641
|
+
self.emit('error', str(e))
|
|
642
|
+
return
|
|
643
|
+
|
|
644
|
+
# Connected
|
|
645
|
+
if want is None:
|
|
646
|
+
self._socks_handshake = None
|
|
647
|
+
return self._try_tls(args)
|
|
648
|
+
|
|
649
|
+
# Switch between reading and writing
|
|
650
|
+
self.reactor.watch(handle, True, want == 'write')
|
|
651
|
+
|
|
652
|
+
def _try_socks(self, args: dict[str, Any]) -> None:
|
|
653
|
+
handle = self._handle
|
|
654
|
+
if not args.get('socks_address'):
|
|
655
|
+
return self._try_tls(args)
|
|
656
|
+
if not SOCKS:
|
|
657
|
+
self.emit('error', 'SOCKS support is disabled')
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
port = int(args.get('port') or (443 if args.get('tls') else 80))
|
|
661
|
+
socks = self._socks_handshake = _Socks(
|
|
662
|
+
handle, args['address'], port, args.get('socks_user'), args.get('socks_pass')
|
|
663
|
+
)
|
|
664
|
+
reactor = self.reactor
|
|
665
|
+
reactor.remove(handle)
|
|
666
|
+
try:
|
|
667
|
+
socks.step()
|
|
668
|
+
except _SocksError as e:
|
|
669
|
+
# Unlike Mojolicious: the error of the handshake, not "SOCKS upgrade failed", which Mojo gives where the
|
|
670
|
+
# handshake fails before it would block for the first time, so that the message depends on timing
|
|
671
|
+
self.emit('error', str(e))
|
|
672
|
+
return
|
|
673
|
+
|
|
674
|
+
self._wait(self._socks, handle, args)
|
|
675
|
+
|
|
676
|
+
def _try_tls(self, args: dict[str, Any]) -> None:
|
|
677
|
+
handle = self._handle
|
|
678
|
+
if not args.get('tls'):
|
|
679
|
+
self._cleanup()
|
|
680
|
+
self.emit('connect', handle)
|
|
681
|
+
return
|
|
682
|
+
reactor = self.reactor
|
|
683
|
+
reactor.remove(handle)
|
|
684
|
+
self._handle = None
|
|
685
|
+
|
|
686
|
+
# Start TLS handshake
|
|
687
|
+
ref = weakref.ref(self)
|
|
688
|
+
|
|
689
|
+
def upgrade(tls: Any, handle: Any) -> None:
|
|
690
|
+
if (client := ref()) is not None:
|
|
691
|
+
client._cleanup()
|
|
692
|
+
client.emit('connect', handle)
|
|
693
|
+
|
|
694
|
+
def error(tls: Any, err: Any) -> None:
|
|
695
|
+
if (client := ref()) is not None:
|
|
696
|
+
client.emit('error', err)
|
|
697
|
+
|
|
698
|
+
tls = Pyjo.IOLoop.TLS.new(handle)
|
|
699
|
+
tls.reactor = reactor
|
|
700
|
+
tls.on('upgrade', upgrade)
|
|
701
|
+
tls.on('error', error)
|
|
702
|
+
tls.negotiate(**args)
|
|
703
|
+
|
|
704
|
+
def _wait(self, next: Callable[[dict[str, Any]], None], handle: Any, args: dict[str, Any]) -> None:
|
|
705
|
+
ref = weakref.ref(self)
|
|
706
|
+
name = next.__name__
|
|
707
|
+
|
|
708
|
+
def cb(reactor: Any, writable: bool) -> None:
|
|
709
|
+
if (client := ref()) is not None:
|
|
710
|
+
getattr(client, name)(args)
|
|
711
|
+
|
|
712
|
+
self.reactor.io(handle, cb).watch(handle, False, True)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# The name of a host for SOCKS5, as IDNA where it is not ASCII
|
|
716
|
+
def _encode_host(address: str) -> bytes:
|
|
717
|
+
try:
|
|
718
|
+
return address.encode('ascii')
|
|
719
|
+
except UnicodeEncodeError:
|
|
720
|
+
return address.encode('idna')
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
# A connection that does not block
|
|
724
|
+
def _connect_ex(handle: socket.socket, address: Any) -> None:
|
|
725
|
+
err = handle.connect_ex(address)
|
|
726
|
+
if err and err not in (errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EAGAIN):
|
|
727
|
+
raise OSError(err, os.strerror(err))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _port(args: dict[str, Any]) -> int:
|
|
731
|
+
return int(args.get('socks_port') or args.get('port') or (443 if args.get('tls') else 80))
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
new = Pyjo_IOLoop_Client.new
|
|
735
|
+
object = Pyjo_IOLoop_Client
|
|
736
|
+
Client = Pyjo_IOLoop_Client
|