python-socks 2.8.2__py3-none-any.whl → 3.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.
- python_socks/__init__.py +11 -13
- python_socks/_abc.py +35 -22
- python_socks/_connectors/abc.py +5 -3
- python_socks/_connectors/factory_async.py +8 -8
- python_socks/_connectors/factory_sync.py +8 -8
- python_socks/_connectors/http_async.py +7 -7
- python_socks/_connectors/http_sync.py +7 -7
- python_socks/_connectors/socks4_async.py +10 -10
- python_socks/_connectors/socks4_sync.py +10 -10
- python_socks/_connectors/socks5_async.py +29 -26
- python_socks/_connectors/socks5_sync.py +36 -28
- python_socks/_errors.py +15 -2
- python_socks/_helpers.py +35 -32
- python_socks/_protocols/errors.py +10 -1
- python_socks/_protocols/http.py +55 -60
- python_socks/_protocols/socks4.py +22 -26
- python_socks/_protocols/socks5.py +77 -70
- python_socks/_types.py +4 -0
- python_socks/_version.py +2 -2
- python_socks/async_/__init__.py +0 -3
- python_socks/async_/anyio/__init__.py +1 -2
- python_socks/async_/anyio/_connect.py +6 -4
- python_socks/async_/anyio/_proxy.py +85 -92
- python_socks/async_/anyio/_resolver.py +10 -3
- python_socks/async_/anyio/_stream.py +11 -26
- python_socks/async_/anyio/v2/__init__.py +1 -5
- python_socks/async_/anyio/v2/_connect.py +7 -4
- python_socks/async_/anyio/v2/_proxy.py +36 -26
- python_socks/async_/anyio/v2/_stream.py +11 -11
- python_socks/async_/asyncio/__init__.py +1 -2
- python_socks/async_/asyncio/_connect.py +12 -7
- python_socks/async_/asyncio/_proxy.py +48 -49
- python_socks/async_/asyncio/_resolver.py +11 -4
- python_socks/async_/asyncio/_stream.py +9 -13
- python_socks/async_/asyncio/v2/__init__.py +1 -2
- python_socks/async_/asyncio/v2/_connect.py +8 -5
- python_socks/async_/asyncio/v2/_proxy.py +38 -28
- python_socks/async_/asyncio/v2/_stream.py +20 -18
- python_socks/async_/trio/__init__.py +1 -1
- python_socks/async_/trio/_connect.py +5 -4
- python_socks/async_/trio/_proxy.py +46 -48
- python_socks/async_/trio/_resolver.py +11 -3
- python_socks/async_/trio/_stream.py +9 -9
- python_socks/async_/trio/v2/__init__.py +1 -5
- python_socks/async_/trio/v2/_connect.py +3 -2
- python_socks/async_/trio/v2/_proxy.py +34 -24
- python_socks/async_/trio/v2/_stream.py +13 -13
- python_socks/sync/__init__.py +1 -3
- python_socks/sync/_connect.py +4 -3
- python_socks/sync/_proxy.py +42 -43
- python_socks/sync/_resolver.py +17 -5
- python_socks/sync/_stream.py +9 -9
- python_socks/sync/v2/__init__.py +1 -5
- python_socks/sync/v2/_connect.py +5 -3
- python_socks/sync/v2/_proxy.py +35 -26
- python_socks/sync/v2/_ssl_transport.py +3 -0
- python_socks/sync/v2/_stream.py +19 -14
- python_socks-3.1.0.dist-info/METADATA +346 -0
- python_socks-3.1.0.dist-info/RECORD +65 -0
- {python_socks-2.8.2.dist-info → python_socks-3.1.0.dist-info}/WHEEL +1 -1
- python_socks/async_/_proxy_chain.py +0 -34
- python_socks/async_/anyio/_chain.py +0 -42
- python_socks/async_/anyio/v2/_chain.py +0 -32
- python_socks/async_/asyncio/v2/_chain.py +0 -32
- python_socks/async_/curio/__init__.py +0 -4
- python_socks/async_/curio/_connect.py +0 -17
- python_socks/async_/curio/_proxy.py +0 -132
- python_socks/async_/curio/_resolver.py +0 -25
- python_socks/async_/curio/_stream.py +0 -32
- python_socks/async_/trio/v2/_chain.py +0 -32
- python_socks/sync/_chain.py +0 -32
- python_socks/sync/v2/_chain.py +0 -26
- python_socks-2.8.2.dist-info/METADATA +0 -295
- python_socks-2.8.2.dist-info/RECORD +0 -77
- {python_socks-2.8.2.dist-info → python_socks-3.1.0.dist-info}/licenses/LICENSE.txt +0 -0
- {python_socks-2.8.2.dist-info → python_socks-3.1.0.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
# ruff:noqa:TRY004
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
1
4
|
import enum
|
|
2
|
-
from functools import singledispatchmethod
|
|
3
5
|
import ipaddress
|
|
4
6
|
import socket
|
|
5
|
-
from typing import Optional, Type, Union
|
|
6
7
|
from dataclasses import dataclass, field
|
|
8
|
+
from functools import singledispatchmethod
|
|
9
|
+
from typing import Union, final
|
|
7
10
|
|
|
8
|
-
from .errors import ReplyError
|
|
9
11
|
from .._helpers import is_ip_address
|
|
12
|
+
from .errors import ReplyError
|
|
10
13
|
|
|
11
14
|
RSV = NULL = AUTH_GRANTED = 0x00
|
|
12
15
|
SOCKS_VER = 0x05
|
|
@@ -25,13 +28,13 @@ class AddressType(enum.IntEnum):
|
|
|
25
28
|
IPV6 = 0x04
|
|
26
29
|
|
|
27
30
|
@classmethod
|
|
28
|
-
def from_ip_ver(cls, ver: int):
|
|
29
|
-
if ver == 4:
|
|
31
|
+
def from_ip_ver(cls, ver: int) -> AddressType:
|
|
32
|
+
if ver == 4: # noqa: PLR2004
|
|
30
33
|
return cls.IPV4
|
|
31
|
-
if ver == 6:
|
|
34
|
+
if ver == 6: # noqa: PLR2004
|
|
32
35
|
return cls.IPV6
|
|
33
36
|
|
|
34
|
-
raise ValueError(
|
|
37
|
+
raise ValueError("Invalid IP version")
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
class Command(enum.IntEnum):
|
|
@@ -53,25 +56,25 @@ class ReplyCode(enum.IntEnum):
|
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
ReplyMessages = {
|
|
56
|
-
ReplyCode.SUCCEEDED:
|
|
57
|
-
ReplyCode.GENERAL_FAILURE:
|
|
58
|
-
ReplyCode.CONNECTION_NOT_ALLOWED:
|
|
59
|
-
ReplyCode.NETWORK_UNREACHABLE:
|
|
60
|
-
ReplyCode.HOST_UNREACHABLE:
|
|
61
|
-
ReplyCode.CONNECTION_REFUSED:
|
|
62
|
-
ReplyCode.TTL_EXPIRED:
|
|
63
|
-
ReplyCode.COMMAND_NOT_SUPPORTED:
|
|
64
|
-
ReplyCode.ADDRESS_TYPE_NOT_SUPPORTED:
|
|
59
|
+
ReplyCode.SUCCEEDED: "Request granted",
|
|
60
|
+
ReplyCode.GENERAL_FAILURE: "General SOCKS server failure",
|
|
61
|
+
ReplyCode.CONNECTION_NOT_ALLOWED: "Connection not allowed by ruleset",
|
|
62
|
+
ReplyCode.NETWORK_UNREACHABLE: "Network unreachable",
|
|
63
|
+
ReplyCode.HOST_UNREACHABLE: "Host unreachable",
|
|
64
|
+
ReplyCode.CONNECTION_REFUSED: "Connection refused by destination host",
|
|
65
|
+
ReplyCode.TTL_EXPIRED: "TTL expired",
|
|
66
|
+
ReplyCode.COMMAND_NOT_SUPPORTED: "Command not supported or protocol error",
|
|
67
|
+
ReplyCode.ADDRESS_TYPE_NOT_SUPPORTED: "Address type not supported",
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
@dataclass
|
|
69
72
|
class AuthMethodsRequest:
|
|
70
|
-
username:
|
|
71
|
-
password:
|
|
73
|
+
username: str | None
|
|
74
|
+
password: str | None
|
|
72
75
|
methods: bytearray = field(init=False)
|
|
73
76
|
|
|
74
|
-
def __post_init__(self):
|
|
77
|
+
def __post_init__(self) -> None:
|
|
75
78
|
methods = bytearray([AuthMethod.ANONYMOUS])
|
|
76
79
|
|
|
77
80
|
if self.username and self.password:
|
|
@@ -90,26 +93,26 @@ class AuthMethodReply:
|
|
|
90
93
|
ver: int
|
|
91
94
|
method: AuthMethod
|
|
92
95
|
|
|
93
|
-
def validate(self, request: AuthMethodsRequest):
|
|
96
|
+
def validate(self, request: AuthMethodsRequest) -> None:
|
|
94
97
|
if self.method not in request.methods: # pragma: no cover
|
|
95
|
-
raise ReplyError(f
|
|
98
|
+
raise ReplyError(f"Unexpected SOCKS authentication method: {self.method}")
|
|
96
99
|
|
|
97
100
|
@classmethod
|
|
98
|
-
def loads(cls, data: bytes) ->
|
|
101
|
+
def loads(cls, data: bytes) -> AuthMethodReply:
|
|
99
102
|
if len(data) != cls.SIZE:
|
|
100
|
-
raise ReplyError(
|
|
103
|
+
raise ReplyError("Malformed authentication method reply")
|
|
101
104
|
|
|
102
105
|
ver = data[0]
|
|
103
106
|
if ver != SOCKS_VER: # pragma: no cover
|
|
104
|
-
raise ReplyError(f
|
|
107
|
+
raise ReplyError(f"Unexpected SOCKS version number: {ver}")
|
|
105
108
|
|
|
106
109
|
try:
|
|
107
110
|
method = AuthMethod(data[1])
|
|
108
|
-
except ValueError:
|
|
109
|
-
raise ReplyError(f
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
raise ReplyError(f"Invalid authentication method: {data[1]:#02X}") from e
|
|
110
113
|
|
|
111
114
|
if method == AuthMethod.NO_ACCEPTABLE: # pragma: no cover
|
|
112
|
-
raise ReplyError(
|
|
115
|
+
raise ReplyError("No acceptable authentication methods were offered")
|
|
113
116
|
|
|
114
117
|
return cls(ver=ver, method=method)
|
|
115
118
|
|
|
@@ -125,9 +128,9 @@ class AuthRequest:
|
|
|
125
128
|
data = bytearray()
|
|
126
129
|
data.append(self.VER)
|
|
127
130
|
data.append(len(self.username))
|
|
128
|
-
data += self.username.encode(
|
|
131
|
+
data += self.username.encode("ascii")
|
|
129
132
|
data.append(len(self.password))
|
|
130
|
-
data += self.password.encode(
|
|
133
|
+
data += self.password.encode("ascii")
|
|
131
134
|
return bytes(data)
|
|
132
135
|
|
|
133
136
|
|
|
@@ -139,17 +142,17 @@ class AuthReply:
|
|
|
139
142
|
status: int
|
|
140
143
|
|
|
141
144
|
@classmethod
|
|
142
|
-
def loads(cls, data: bytes) ->
|
|
145
|
+
def loads(cls, data: bytes) -> AuthReply:
|
|
143
146
|
if len(data) != cls.SIZE:
|
|
144
|
-
raise ReplyError(
|
|
147
|
+
raise ReplyError("Malformed auth reply")
|
|
145
148
|
|
|
146
149
|
ver = data[0]
|
|
147
150
|
if ver != AuthRequest.VER: # pragma: no cover
|
|
148
|
-
raise ReplyError(
|
|
151
|
+
raise ReplyError("Invalid authentication response")
|
|
149
152
|
|
|
150
153
|
status = data[1]
|
|
151
154
|
if status != AUTH_GRANTED: # pragma: no cover
|
|
152
|
-
raise ReplyError(
|
|
155
|
+
raise ReplyError("Username and password authentication failure")
|
|
153
156
|
|
|
154
157
|
return cls(ver=ver, status=status)
|
|
155
158
|
|
|
@@ -165,15 +168,15 @@ class ConnectRequest:
|
|
|
165
168
|
return bytes(data)
|
|
166
169
|
|
|
167
170
|
def _build_addr_request(self) -> bytes:
|
|
168
|
-
port = self.port.to_bytes(2,
|
|
171
|
+
port = self.port.to_bytes(2, "big")
|
|
169
172
|
|
|
170
173
|
if is_ip_address(self.host):
|
|
171
174
|
ip = ipaddress.ip_address(self.host)
|
|
172
175
|
address_type = AddressType.from_ip_ver(ip.version)
|
|
173
176
|
return bytes([address_type]) + ip.packed + port
|
|
174
|
-
else:
|
|
177
|
+
else: # noqa: RET505
|
|
175
178
|
address_type = AddressType.DOMAIN
|
|
176
|
-
host = self.host.encode(
|
|
179
|
+
host = self.host.encode("idna")
|
|
177
180
|
return bytes([address_type, len(host)]) + host + port
|
|
178
181
|
|
|
179
182
|
|
|
@@ -185,43 +188,43 @@ class ConnectReply:
|
|
|
185
188
|
bound_host: str
|
|
186
189
|
bound_port: int
|
|
187
190
|
|
|
188
|
-
def validate(self):
|
|
191
|
+
def validate(self) -> None:
|
|
189
192
|
pass
|
|
190
193
|
|
|
191
194
|
@classmethod
|
|
192
|
-
def loads(cls, data: bytes) ->
|
|
195
|
+
def loads(cls, data: bytes) -> ConnectReply: # noqa: C901
|
|
193
196
|
if not data:
|
|
194
|
-
raise ReplyError(
|
|
197
|
+
raise ReplyError("Empty connect reply")
|
|
195
198
|
|
|
196
199
|
ver = data[0]
|
|
197
200
|
if ver != SOCKS_VER: # pragma: no cover
|
|
198
|
-
raise ReplyError(f
|
|
201
|
+
raise ReplyError(f"Unexpected SOCKS version number: {ver:#02X}")
|
|
199
202
|
|
|
200
203
|
try:
|
|
201
204
|
reply = ReplyCode(data[1])
|
|
202
|
-
except IndexError:
|
|
203
|
-
raise ReplyError(
|
|
204
|
-
except ValueError:
|
|
205
|
-
raise ReplyError(f
|
|
205
|
+
except IndexError as e:
|
|
206
|
+
raise ReplyError("Malformed connect reply") from e
|
|
207
|
+
except ValueError as e:
|
|
208
|
+
raise ReplyError(f"Invalid reply code: {data[1]:#02X}") from e
|
|
206
209
|
|
|
207
210
|
if reply != ReplyCode.SUCCEEDED: # pragma: no cover
|
|
208
|
-
msg = ReplyMessages.get(reply,
|
|
211
|
+
msg = ReplyMessages.get(reply, "Unknown error")
|
|
209
212
|
raise ReplyError(msg, error_code=reply)
|
|
210
213
|
|
|
211
214
|
try:
|
|
212
215
|
rsv = data[2]
|
|
213
|
-
except IndexError:
|
|
214
|
-
raise ReplyError(
|
|
216
|
+
except IndexError as e:
|
|
217
|
+
raise ReplyError("Malformed connect reply") from e
|
|
215
218
|
|
|
216
219
|
if rsv != RSV: # pragma: no cover
|
|
217
|
-
raise ReplyError(f
|
|
220
|
+
raise ReplyError(f"The reserved byte must be {RSV:#02X}")
|
|
218
221
|
|
|
219
222
|
try:
|
|
220
223
|
addr_type = data[3]
|
|
221
224
|
bnd_host_data = data[4:-2]
|
|
222
225
|
bnd_port_data = data[-2:]
|
|
223
|
-
except IndexError:
|
|
224
|
-
raise ReplyError(
|
|
226
|
+
except IndexError as e:
|
|
227
|
+
raise ReplyError("Malformed connect reply") from e
|
|
225
228
|
|
|
226
229
|
if addr_type == AddressType.IPV4:
|
|
227
230
|
bnd_host = socket.inet_ntop(socket.AF_INET, bnd_host_data)
|
|
@@ -231,9 +234,9 @@ class ConnectReply:
|
|
|
231
234
|
# host_len = bnd_host_data[0]
|
|
232
235
|
bnd_host = bnd_host_data[1:].decode()
|
|
233
236
|
else: # pragma: no cover
|
|
234
|
-
raise ReplyError(f
|
|
237
|
+
raise ReplyError(f"Invalid address type: {addr_type:#02X}")
|
|
235
238
|
|
|
236
|
-
bnd_port = int.from_bytes(bnd_port_data,
|
|
239
|
+
bnd_port = int.from_bytes(bnd_port_data, "big")
|
|
237
240
|
|
|
238
241
|
return cls(
|
|
239
242
|
ver=ver,
|
|
@@ -244,35 +247,42 @@ class ConnectReply:
|
|
|
244
247
|
)
|
|
245
248
|
|
|
246
249
|
|
|
250
|
+
@final
|
|
247
251
|
class StateServerWaitingForAuthMethods:
|
|
248
252
|
pass
|
|
249
253
|
|
|
250
254
|
|
|
255
|
+
@final
|
|
251
256
|
@dataclass
|
|
252
257
|
class StateClientSentAuthMethods:
|
|
253
258
|
data: AuthMethodsRequest
|
|
254
259
|
|
|
255
260
|
|
|
261
|
+
@final
|
|
256
262
|
@dataclass
|
|
257
263
|
class StateServerWaitingForAuth:
|
|
258
264
|
data: AuthMethodReply
|
|
259
265
|
|
|
260
266
|
|
|
267
|
+
@final
|
|
261
268
|
@dataclass
|
|
262
269
|
class StateClientAuthenticated:
|
|
263
|
-
data:
|
|
270
|
+
data: AuthReply | None = None
|
|
264
271
|
|
|
265
272
|
|
|
273
|
+
@final
|
|
266
274
|
@dataclass
|
|
267
275
|
class StateClientSentAuthRequest:
|
|
268
276
|
data: AuthRequest
|
|
269
277
|
|
|
270
278
|
|
|
279
|
+
@final
|
|
271
280
|
@dataclass
|
|
272
281
|
class StateClientSentConnectRequest:
|
|
273
282
|
data: ConnectRequest
|
|
274
283
|
|
|
275
284
|
|
|
285
|
+
@final
|
|
276
286
|
@dataclass
|
|
277
287
|
class StateServerConnected:
|
|
278
288
|
data: ConnectReply
|
|
@@ -302,38 +312,38 @@ ConnectionState = Union[
|
|
|
302
312
|
|
|
303
313
|
|
|
304
314
|
class Connection:
|
|
305
|
-
def __init__(self):
|
|
306
|
-
self._state = StateServerWaitingForAuthMethods()
|
|
315
|
+
def __init__(self) -> None:
|
|
316
|
+
self._state: ConnectionState = StateServerWaitingForAuthMethods()
|
|
307
317
|
|
|
308
318
|
@singledispatchmethod
|
|
309
319
|
def send(self, request: Request) -> bytes:
|
|
310
|
-
raise RuntimeError(f
|
|
320
|
+
raise RuntimeError(f"Invalid request type: {request.__class__}")
|
|
311
321
|
|
|
312
322
|
@send.register
|
|
313
323
|
def _send_auth_methods(self, request: AuthMethodsRequest) -> bytes:
|
|
314
|
-
if not self.
|
|
315
|
-
raise RuntimeError(
|
|
324
|
+
if not isinstance(self._state, StateServerWaitingForAuthMethods):
|
|
325
|
+
raise RuntimeError("Server is not currently waiting for auth methods")
|
|
316
326
|
self._state = StateClientSentAuthMethods(request)
|
|
317
327
|
return request.dumps()
|
|
318
328
|
|
|
319
329
|
@send.register
|
|
320
330
|
def _send_auth(self, request: AuthRequest) -> bytes:
|
|
321
|
-
if not self.
|
|
322
|
-
raise RuntimeError(
|
|
331
|
+
if not isinstance(self._state, StateServerWaitingForAuth):
|
|
332
|
+
raise RuntimeError("Server is not currently waiting for authentication")
|
|
323
333
|
self._state = StateClientSentAuthRequest(request)
|
|
324
334
|
return request.dumps()
|
|
325
335
|
|
|
326
336
|
@send.register
|
|
327
337
|
def _send_connect(self, request: ConnectRequest) -> bytes:
|
|
328
|
-
if not self.
|
|
329
|
-
raise RuntimeError(
|
|
338
|
+
if not isinstance(self._state, StateClientAuthenticated):
|
|
339
|
+
raise RuntimeError("Client is not authenticated")
|
|
330
340
|
self._state = StateClientSentConnectRequest(request)
|
|
331
341
|
return request.dumps()
|
|
332
342
|
|
|
333
343
|
def receive(self, data: bytes) -> Reply:
|
|
334
344
|
reply: Reply
|
|
335
345
|
|
|
336
|
-
if self.
|
|
346
|
+
if isinstance(self._state, StateClientSentAuthMethods):
|
|
337
347
|
reply = AuthMethodReply.loads(data)
|
|
338
348
|
reply.validate(self._state.data)
|
|
339
349
|
if reply.method == AuthMethod.USERNAME_PASSWORD:
|
|
@@ -342,21 +352,18 @@ class Connection:
|
|
|
342
352
|
self._state = StateClientAuthenticated()
|
|
343
353
|
return reply
|
|
344
354
|
|
|
345
|
-
if self.
|
|
355
|
+
if isinstance(self._state, StateClientSentAuthRequest):
|
|
346
356
|
reply = AuthReply.loads(data)
|
|
347
357
|
self._state = StateClientAuthenticated(data=reply)
|
|
348
358
|
return reply
|
|
349
359
|
|
|
350
|
-
if self.
|
|
360
|
+
if isinstance(self._state, StateClientSentConnectRequest):
|
|
351
361
|
reply = ConnectReply.loads(data)
|
|
352
362
|
self._state = StateServerConnected(data=reply)
|
|
353
363
|
return reply
|
|
354
364
|
|
|
355
|
-
raise RuntimeError(f
|
|
356
|
-
|
|
357
|
-
def _state_is(self, state_cls: Type[ConnectionState]):
|
|
358
|
-
return self.state.__class__ is state_cls
|
|
365
|
+
raise RuntimeError(f"Invalid connection state: {self._state}")
|
|
359
366
|
|
|
360
367
|
@property
|
|
361
|
-
def state(self):
|
|
368
|
+
def state(self) -> ConnectionState:
|
|
362
369
|
return self._state
|
python_socks/_types.py
CHANGED
python_socks/_version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__title__ =
|
|
2
|
-
__version__ =
|
|
1
|
+
__title__ = "python-socks"
|
|
2
|
+
__version__ = "3.1.0"
|
python_socks/async_/__init__.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
2
5
|
import anyio
|
|
3
6
|
import anyio.abc
|
|
4
7
|
|
|
@@ -6,11 +9,10 @@ import anyio.abc
|
|
|
6
9
|
async def connect_tcp(
|
|
7
10
|
host: str,
|
|
8
11
|
port: int,
|
|
9
|
-
|
|
12
|
+
**kwargs: Any,
|
|
10
13
|
) -> anyio.abc.SocketStream:
|
|
11
|
-
|
|
12
14
|
return await anyio.connect_tcp(
|
|
13
15
|
remote_host=host,
|
|
14
16
|
remote_port=port,
|
|
15
|
-
|
|
17
|
+
**kwargs,
|
|
16
18
|
)
|
|
@@ -1,137 +1,130 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from asyncio import IncompleteReadError
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import anyio
|
|
7
|
+
import anyio.abc
|
|
6
8
|
|
|
7
|
-
from ...
|
|
9
|
+
from ..._connectors.factory_async import create_connector
|
|
10
|
+
from ..._errors import (
|
|
11
|
+
ProxyConnectionError,
|
|
12
|
+
ProxyError,
|
|
13
|
+
ProxyTimeoutError,
|
|
14
|
+
)
|
|
8
15
|
from ..._helpers import parse_proxy_url
|
|
9
|
-
from ...
|
|
10
|
-
|
|
16
|
+
from ..._protocols.errors import ReplyError
|
|
17
|
+
from ..._types import ProxyType
|
|
18
|
+
from ._connect import connect_tcp
|
|
11
19
|
from ._resolver import Resolver
|
|
12
20
|
from ._stream import AnyioSocketStream
|
|
13
|
-
from ._connect import connect_tcp
|
|
14
|
-
|
|
15
|
-
from ..._protocols.errors import ReplyError
|
|
16
|
-
from ..._connectors.factory_async import create_connector
|
|
17
21
|
|
|
18
22
|
DEFAULT_TIMEOUT = 60
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class AnyioProxy:
|
|
22
|
-
_stream: Optional[AnyioSocketStream]
|
|
23
|
-
|
|
24
26
|
def __init__(
|
|
25
27
|
self,
|
|
26
28
|
proxy_type: ProxyType,
|
|
27
29
|
host: str,
|
|
28
30
|
port: int,
|
|
29
|
-
username:
|
|
30
|
-
password:
|
|
31
|
-
rdns:
|
|
32
|
-
|
|
33
|
-
):
|
|
31
|
+
username: str | None = None,
|
|
32
|
+
password: str | None = None,
|
|
33
|
+
rdns: bool | None = None, # noqa: FBT001
|
|
34
|
+
forward: AnyioProxy | None = None,
|
|
35
|
+
) -> None:
|
|
34
36
|
self._proxy_type = proxy_type
|
|
35
37
|
self._proxy_host = host
|
|
36
38
|
self._proxy_port = port
|
|
37
39
|
self._password = password
|
|
38
40
|
self._username = username
|
|
39
41
|
self._rdns = rdns
|
|
42
|
+
self._forward = forward
|
|
40
43
|
|
|
41
|
-
self._proxy_ssl = proxy_ssl
|
|
42
44
|
self._resolver = Resolver()
|
|
43
45
|
|
|
44
46
|
async def connect(
|
|
45
47
|
self,
|
|
46
48
|
dest_host: str,
|
|
47
49
|
dest_port: int,
|
|
48
|
-
|
|
49
|
-
timeout: Optional[float] = None,
|
|
50
|
+
timeout: float | None = None,
|
|
50
51
|
**kwargs: Any,
|
|
51
|
-
) ->
|
|
52
|
+
) -> anyio.abc.SocketStream:
|
|
52
53
|
if timeout is None:
|
|
53
54
|
timeout = DEFAULT_TIMEOUT
|
|
54
55
|
|
|
55
|
-
_stream = kwargs.get('_stream')
|
|
56
|
-
if _stream is not None:
|
|
57
|
-
warnings.warn(
|
|
58
|
-
"The '_stream' argument is deprecated and will be removed in the future",
|
|
59
|
-
DeprecationWarning,
|
|
60
|
-
stacklevel=2,
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
local_host = kwargs.get('local_host')
|
|
64
56
|
try:
|
|
65
57
|
with anyio.fail_after(timeout):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
port=self._proxy_port,
|
|
72
|
-
local_host=local_host,
|
|
73
|
-
)
|
|
74
|
-
)
|
|
75
|
-
except OSError as e:
|
|
76
|
-
msg = 'Could not connect to proxy {}:{} [{}]'.format(
|
|
77
|
-
self._proxy_host,
|
|
78
|
-
self._proxy_port,
|
|
79
|
-
e.strerror,
|
|
80
|
-
)
|
|
81
|
-
raise ProxyConnectionError(e.errno, msg) from e
|
|
82
|
-
|
|
83
|
-
stream = _stream
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
if self._proxy_ssl is not None:
|
|
87
|
-
stream = await stream.start_tls(
|
|
88
|
-
hostname=self._proxy_host,
|
|
89
|
-
ssl_context=self._proxy_ssl,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
connector = create_connector(
|
|
93
|
-
proxy_type=self._proxy_type,
|
|
94
|
-
username=self._username,
|
|
95
|
-
password=self._password,
|
|
96
|
-
rdns=self._rdns,
|
|
97
|
-
resolver=self._resolver,
|
|
98
|
-
)
|
|
99
|
-
await connector.connect(
|
|
100
|
-
stream=stream,
|
|
101
|
-
host=dest_host,
|
|
102
|
-
port=dest_port,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
if dest_ssl is not None:
|
|
106
|
-
stream = await stream.start_tls(
|
|
107
|
-
hostname=dest_host,
|
|
108
|
-
ssl_context=dest_ssl,
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
return stream
|
|
112
|
-
except ReplyError as e:
|
|
113
|
-
await stream.close()
|
|
114
|
-
raise ProxyError(e, error_code=e.error_code)
|
|
115
|
-
except BaseException:
|
|
116
|
-
await stream.close()
|
|
117
|
-
raise
|
|
118
|
-
|
|
58
|
+
return await self._connect(
|
|
59
|
+
dest_host=dest_host,
|
|
60
|
+
dest_port=dest_port,
|
|
61
|
+
**kwargs,
|
|
62
|
+
)
|
|
119
63
|
except TimeoutError as e:
|
|
120
|
-
raise ProxyTimeoutError(f
|
|
64
|
+
raise ProxyTimeoutError(f"Proxy connection timed out: {timeout}") from e
|
|
65
|
+
|
|
66
|
+
async def _connect(
|
|
67
|
+
self,
|
|
68
|
+
dest_host: str,
|
|
69
|
+
dest_port: int,
|
|
70
|
+
**kwargs: Any,
|
|
71
|
+
) -> anyio.abc.SocketStream:
|
|
72
|
+
if self._forward is not None:
|
|
73
|
+
_stream = await self._forward.connect(
|
|
74
|
+
dest_host=self._proxy_host,
|
|
75
|
+
dest_port=self._proxy_port,
|
|
76
|
+
timeout=None,
|
|
77
|
+
**kwargs,
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
try:
|
|
81
|
+
_stream = await connect_tcp(
|
|
82
|
+
host=self._proxy_host,
|
|
83
|
+
port=self._proxy_port,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
except OSError as e:
|
|
87
|
+
msg = (
|
|
88
|
+
f"Could not connect to proxy "
|
|
89
|
+
f"{self._proxy_host}:{self._proxy_port} [{e.strerror}]"
|
|
90
|
+
)
|
|
91
|
+
raise ProxyConnectionError(e.errno, msg) from e
|
|
92
|
+
|
|
93
|
+
stream = AnyioSocketStream(_stream)
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
95
|
+
try:
|
|
96
|
+
connector = create_connector(
|
|
97
|
+
proxy_type=self._proxy_type,
|
|
98
|
+
username=self._username,
|
|
99
|
+
password=self._password,
|
|
100
|
+
rdns=self._rdns,
|
|
101
|
+
resolver=self._resolver,
|
|
102
|
+
)
|
|
103
|
+
await connector.connect(
|
|
104
|
+
stream=stream,
|
|
105
|
+
host=dest_host,
|
|
106
|
+
port=dest_port,
|
|
107
|
+
)
|
|
125
108
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
return _stream
|
|
110
|
+
except ReplyError as e:
|
|
111
|
+
await stream.close()
|
|
112
|
+
raise ProxyError(e, error_code=e.error_code) from e
|
|
113
|
+
except IncompleteReadError as e:
|
|
114
|
+
await stream.close()
|
|
115
|
+
raise ProxyError(e) from e
|
|
116
|
+
except BaseException:
|
|
117
|
+
with anyio.CancelScope(shield=True):
|
|
118
|
+
await stream.close()
|
|
119
|
+
raise
|
|
129
120
|
|
|
130
121
|
@classmethod
|
|
131
|
-
def create(
|
|
122
|
+
def create(
|
|
123
|
+
cls, *args: Any, **kwargs: Any
|
|
124
|
+
) -> AnyioProxy: # for backward compatibility
|
|
132
125
|
return cls(*args, **kwargs)
|
|
133
126
|
|
|
134
127
|
@classmethod
|
|
135
|
-
def from_url(cls, url: str, **kwargs) ->
|
|
128
|
+
def from_url(cls, url: str, **kwargs: Any) -> AnyioProxy:
|
|
136
129
|
url_args = parse_proxy_url(url)
|
|
137
130
|
return cls(*url_args, **kwargs)
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import anyio
|
|
2
1
|
import socket
|
|
3
2
|
|
|
3
|
+
import anyio
|
|
4
|
+
|
|
4
5
|
from ... import _abc as abc
|
|
6
|
+
from ..._types import ResolvedAddress
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class Resolver(abc.AsyncResolver):
|
|
8
|
-
async def resolve(
|
|
10
|
+
async def resolve(
|
|
11
|
+
self,
|
|
12
|
+
host: str,
|
|
13
|
+
port: int = 0,
|
|
14
|
+
family: socket.AddressFamily = socket.AF_UNSPEC,
|
|
15
|
+
) -> ResolvedAddress:
|
|
9
16
|
infos = await anyio.getaddrinfo(
|
|
10
17
|
host=host,
|
|
11
18
|
port=port,
|
|
@@ -14,7 +21,7 @@ class Resolver(abc.AsyncResolver):
|
|
|
14
21
|
)
|
|
15
22
|
|
|
16
23
|
if not infos: # pragma: no cover
|
|
17
|
-
raise OSError(
|
|
24
|
+
raise OSError(f"Can`t resolve address {host}:{port} [{family}]")
|
|
18
25
|
|
|
19
26
|
infos = sorted(infos, key=lambda info: info[0])
|
|
20
27
|
|