python-socks 2.8.2__py3-none-any.whl → 3.0.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 +16 -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.0.0.dist-info/METADATA +346 -0
- python_socks-3.0.0.dist-info/RECORD +65 -0
- {python_socks-2.8.2.dist-info → python_socks-3.0.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.0.0.dist-info}/licenses/LICENSE.txt +0 -0
- {python_socks-2.8.2.dist-info → python_socks-3.0.0.dist-info}/top_level.txt +0 -0
python_socks/_helpers.py
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
import re
|
|
3
|
-
from
|
|
4
|
-
from urllib.parse import urlparse, unquote
|
|
5
|
+
from urllib.parse import unquote, urlparse
|
|
5
6
|
|
|
6
7
|
from ._types import ProxyType
|
|
7
8
|
|
|
8
|
-
# pylint:disable-next=invalid-name
|
|
9
9
|
_ipv4_pattern = (
|
|
10
|
-
r
|
|
11
|
-
r
|
|
10
|
+
r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
|
|
11
|
+
r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
# pylint:disable-next=invalid-name
|
|
15
14
|
_ipv6_pattern = (
|
|
16
|
-
r
|
|
17
|
-
r
|
|
18
|
-
r
|
|
19
|
-
r
|
|
20
|
-
r
|
|
21
|
-
r
|
|
22
|
-
r
|
|
23
|
-
r
|
|
15
|
+
r"^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}"
|
|
16
|
+
r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)"
|
|
17
|
+
r"((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})"
|
|
18
|
+
r"(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}"
|
|
19
|
+
r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}"
|
|
20
|
+
r"[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)"
|
|
21
|
+
r"(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}"
|
|
22
|
+
r":|:(:[A-F0-9]{1,4}){7})$"
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
_ipv4_regex = re.compile(_ipv4_pattern)
|
|
27
26
|
_ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE)
|
|
28
|
-
_ipv4_regexb = re.compile(_ipv4_pattern.encode(
|
|
29
|
-
_ipv6_regexb = re.compile(_ipv6_pattern.encode(
|
|
27
|
+
_ipv4_regexb = re.compile(_ipv4_pattern.encode("ascii"))
|
|
28
|
+
_ipv6_regexb = re.compile(_ipv6_pattern.encode("ascii"), flags=re.IGNORECASE)
|
|
30
29
|
|
|
31
30
|
|
|
32
|
-
def _is_ip_address(
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
def _is_ip_address(
|
|
32
|
+
regex: re.Pattern[str],
|
|
33
|
+
regexb: re.Pattern[bytes],
|
|
34
|
+
host: str | bytes | bytearray | memoryview,
|
|
35
|
+
) -> bool:
|
|
35
36
|
if isinstance(host, str):
|
|
36
37
|
return bool(regex.match(host))
|
|
37
|
-
elif isinstance(host, (bytes, bytearray, memoryview)):
|
|
38
|
+
elif isinstance(host, (bytes, bytearray, memoryview)): # noqa: RET505
|
|
38
39
|
return bool(regexb.match(host))
|
|
39
40
|
else:
|
|
40
|
-
raise TypeError(
|
|
41
|
-
|
|
41
|
+
raise TypeError( # pragma: no cover
|
|
42
|
+
f"{host} [{type(host)}] is not a str or bytes"
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
|
|
@@ -46,36 +47,38 @@ is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb)
|
|
|
46
47
|
is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb)
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def is_ip_address(host):
|
|
50
|
+
def is_ip_address(host: str | bytes) -> bool:
|
|
50
51
|
return is_ipv4_address(host) or is_ipv6_address(host)
|
|
51
52
|
|
|
52
53
|
|
|
53
|
-
def parse_proxy_url(
|
|
54
|
+
def parse_proxy_url(
|
|
55
|
+
url: str,
|
|
56
|
+
) -> tuple[ProxyType, str, int, str | None, str | None]:
|
|
54
57
|
parsed = urlparse(url)
|
|
55
58
|
|
|
56
59
|
scheme = parsed.scheme
|
|
57
|
-
if scheme ==
|
|
60
|
+
if scheme == "socks5":
|
|
58
61
|
proxy_type = ProxyType.SOCKS5
|
|
59
|
-
elif scheme ==
|
|
62
|
+
elif scheme == "socks4":
|
|
60
63
|
proxy_type = ProxyType.SOCKS4
|
|
61
|
-
elif scheme ==
|
|
64
|
+
elif scheme == "http":
|
|
62
65
|
proxy_type = ProxyType.HTTP
|
|
63
66
|
else:
|
|
64
|
-
raise ValueError(f
|
|
67
|
+
raise ValueError(f"Invalid scheme component: {scheme}") # pragma: no cover
|
|
65
68
|
|
|
66
69
|
host = parsed.hostname
|
|
67
70
|
if not host:
|
|
68
|
-
raise ValueError(
|
|
71
|
+
raise ValueError("Empty host component") # pragma: no cover
|
|
69
72
|
|
|
70
73
|
try:
|
|
71
74
|
port = parsed.port
|
|
72
75
|
assert port is not None
|
|
73
76
|
except (ValueError, TypeError, AssertionError) as e: # pragma: no cover
|
|
74
|
-
raise ValueError(
|
|
77
|
+
raise ValueError("Invalid port component") from e
|
|
75
78
|
|
|
76
79
|
try:
|
|
77
|
-
username, password = (unquote(parsed.username), unquote(parsed.password))
|
|
80
|
+
username, password = (unquote(parsed.username), unquote(parsed.password)) # type:ignore[arg-type]
|
|
78
81
|
except (AttributeError, TypeError):
|
|
79
|
-
username, password =
|
|
82
|
+
username, password = "", ""
|
|
80
83
|
|
|
81
84
|
return proxy_type, host, port, username, password
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
class ReplyError(Exception):
|
|
2
|
-
def __init__(
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
message: Any,
|
|
10
|
+
error_code: int | None = None,
|
|
11
|
+
) -> None:
|
|
3
12
|
super().__init__(message)
|
|
4
13
|
self.error_code = error_code
|
python_socks/_protocols/http.py
CHANGED
|
@@ -1,86 +1,82 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
3
|
import base64
|
|
4
4
|
import binascii
|
|
5
|
-
|
|
6
|
-
from
|
|
5
|
+
import sys
|
|
6
|
+
from dataclasses import dataclass
|
|
7
7
|
|
|
8
|
-
from .._version import __title__, __version__
|
|
9
8
|
from .._helpers import is_ipv6_address
|
|
10
|
-
|
|
9
|
+
from .._version import __title__, __version__
|
|
11
10
|
from .errors import ReplyError
|
|
12
11
|
|
|
13
|
-
DEFAULT_USER_AGENT =
|
|
14
|
-
sys.version_info
|
|
15
|
-
__title__,
|
|
16
|
-
__version__,
|
|
12
|
+
DEFAULT_USER_AGENT = (
|
|
13
|
+
f"Python/{sys.version_info[0]}.{sys.version_info[1]} {__title__}/{__version__}"
|
|
17
14
|
)
|
|
18
|
-
|
|
19
|
-
CRLF = '\r\n'
|
|
15
|
+
CRLF = "\r\n"
|
|
20
16
|
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
@dataclass
|
|
19
|
+
class BasicAuth:
|
|
20
|
+
login: str
|
|
21
|
+
password: str
|
|
22
|
+
encoding: str = "latin1"
|
|
24
23
|
|
|
25
|
-
def
|
|
26
|
-
if login is None:
|
|
27
|
-
raise ValueError(
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if self.login is None:
|
|
26
|
+
raise ValueError("None is not allowed as login value")
|
|
28
27
|
|
|
29
|
-
if password is None:
|
|
30
|
-
raise ValueError(
|
|
28
|
+
if self.password is None:
|
|
29
|
+
raise ValueError("None is not allowed as password value")
|
|
31
30
|
|
|
32
|
-
if
|
|
31
|
+
if ":" in self.login:
|
|
33
32
|
raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
|
|
34
33
|
|
|
35
|
-
# noinspection PyTypeChecker,PyArgumentList
|
|
36
|
-
return super().__new__(cls, login, password, encoding)
|
|
37
|
-
|
|
38
34
|
@classmethod
|
|
39
|
-
def decode(cls, auth_header: str, encoding: str =
|
|
35
|
+
def decode(cls, auth_header: str, encoding: str = "latin1") -> BasicAuth:
|
|
40
36
|
"""Create a BasicAuth object from an Authorization HTTP header."""
|
|
41
37
|
try:
|
|
42
|
-
auth_type, encoded_credentials = auth_header.split(
|
|
38
|
+
auth_type, encoded_credentials = auth_header.split(" ", 1)
|
|
43
39
|
except ValueError:
|
|
44
|
-
raise ValueError(
|
|
40
|
+
raise ValueError("Could not parse authorization header.") # noqa: B904
|
|
45
41
|
|
|
46
|
-
if auth_type.lower() !=
|
|
47
|
-
raise ValueError(
|
|
42
|
+
if auth_type.lower() != "basic":
|
|
43
|
+
raise ValueError(f"Unknown authorization method {auth_type}")
|
|
48
44
|
|
|
49
45
|
try:
|
|
50
|
-
decoded = base64.b64decode(
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
decoded = base64.b64decode(
|
|
47
|
+
encoded_credentials.encode("ascii"),
|
|
48
|
+
validate=True,
|
|
49
|
+
).decode(encoding)
|
|
53
50
|
except binascii.Error:
|
|
54
|
-
raise ValueError(
|
|
51
|
+
raise ValueError("Invalid base64 encoding.") # noqa: B904
|
|
55
52
|
|
|
56
53
|
try:
|
|
57
54
|
# RFC 2617 HTTP Authentication
|
|
58
55
|
# https://www.ietf.org/rfc/rfc2617.txt
|
|
59
56
|
# the colon must be present, but the username and password may be
|
|
60
57
|
# otherwise blank.
|
|
61
|
-
username, password = decoded.split(
|
|
58
|
+
username, password = decoded.split(":", 1)
|
|
62
59
|
except ValueError:
|
|
63
|
-
raise ValueError(
|
|
60
|
+
raise ValueError("Invalid credentials.") # noqa: B904
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
return cls(username, password, encoding=encoding)
|
|
62
|
+
return cls(login=username, password=password, encoding=encoding)
|
|
67
63
|
|
|
68
64
|
def encode(self) -> str:
|
|
69
65
|
"""Encode credentials."""
|
|
70
|
-
creds =
|
|
71
|
-
return
|
|
66
|
+
creds = f"{self.login}:{self.password}".encode(self.encoding)
|
|
67
|
+
return f"Basic {base64.b64encode(creds).decode(self.encoding)}"
|
|
72
68
|
|
|
73
69
|
|
|
74
70
|
class _Buffer:
|
|
75
|
-
def __init__(self, encoding: str =
|
|
71
|
+
def __init__(self, encoding: str = "utf-8") -> None:
|
|
76
72
|
self._encoding = encoding
|
|
77
73
|
self._buffer = bytearray()
|
|
78
74
|
|
|
79
|
-
def append_line(self, line: str = ""):
|
|
75
|
+
def append_line(self, line: str = "") -> None:
|
|
80
76
|
if line:
|
|
81
77
|
self._buffer.extend(line.encode(self._encoding))
|
|
82
78
|
|
|
83
|
-
self._buffer.extend(CRLF.encode(
|
|
79
|
+
self._buffer.extend(CRLF.encode("ascii"))
|
|
84
80
|
|
|
85
81
|
def dumps(self) -> bytes:
|
|
86
82
|
return bytes(self._buffer)
|
|
@@ -90,8 +86,8 @@ class _Buffer:
|
|
|
90
86
|
class ConnectRequest:
|
|
91
87
|
host: str
|
|
92
88
|
port: int
|
|
93
|
-
username:
|
|
94
|
-
password:
|
|
89
|
+
username: str | None
|
|
90
|
+
password: str | None
|
|
95
91
|
|
|
96
92
|
def dumps(self) -> bytes:
|
|
97
93
|
buff = _Buffer()
|
|
@@ -103,14 +99,14 @@ class ConnectRequest:
|
|
|
103
99
|
# with no port — proxies disagree, and several misroute the
|
|
104
100
|
# unbracketed form (typically returning `200 Connection
|
|
105
101
|
# established` against a connection that was never actually made).
|
|
106
|
-
host = f
|
|
107
|
-
buff.append_line(f
|
|
108
|
-
buff.append_line(f
|
|
109
|
-
buff.append_line(f
|
|
102
|
+
host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
|
|
103
|
+
buff.append_line(f"CONNECT {host}:{self.port} HTTP/1.1")
|
|
104
|
+
buff.append_line(f"Host: {host}:{self.port}")
|
|
105
|
+
buff.append_line(f"User-Agent: {DEFAULT_USER_AGENT}")
|
|
110
106
|
|
|
111
107
|
if self.username and self.password:
|
|
112
108
|
auth = BasicAuth(self.username, self.password)
|
|
113
|
-
buff.append_line(f
|
|
109
|
+
buff.append_line(f"Proxy-Authorization: {auth.encode()}")
|
|
114
110
|
|
|
115
111
|
buff.append_line()
|
|
116
112
|
|
|
@@ -123,33 +119,32 @@ class ConnectReply:
|
|
|
123
119
|
message: str
|
|
124
120
|
|
|
125
121
|
@classmethod
|
|
126
|
-
def loads(cls, data: bytes) ->
|
|
122
|
+
def loads(cls, data: bytes) -> ConnectReply:
|
|
127
123
|
if not data:
|
|
128
|
-
raise ReplyError(
|
|
124
|
+
raise ReplyError("Invalid proxy response") # pragma: no cover
|
|
129
125
|
|
|
130
|
-
line = data.split(CRLF.encode(
|
|
131
|
-
line = line.decode(
|
|
126
|
+
line = data.split(CRLF.encode("ascii"), 1)[0]
|
|
127
|
+
line = line.decode("utf-8", "surrogateescape")
|
|
132
128
|
|
|
133
129
|
try:
|
|
134
|
-
|
|
135
|
-
except ValueError: # pragma: no cover
|
|
136
|
-
raise ReplyError(f
|
|
130
|
+
_version, code, *reason = line.split()
|
|
131
|
+
except ValueError as e: # pragma: no cover
|
|
132
|
+
raise ReplyError(f"Invalid status line: {line}") from e
|
|
137
133
|
|
|
138
134
|
try:
|
|
139
135
|
status_code = int(code)
|
|
140
|
-
except ValueError: # pragma: no cover
|
|
141
|
-
raise ReplyError(f
|
|
136
|
+
except ValueError as e: # pragma: no cover
|
|
137
|
+
raise ReplyError(f"Invalid status code: {code}") from e
|
|
142
138
|
|
|
143
139
|
status_message = " ".join(reason)
|
|
144
140
|
|
|
145
|
-
if status_code != 200:
|
|
146
|
-
msg = f
|
|
141
|
+
if status_code != 200: # noqa: PLR2004
|
|
142
|
+
msg = f"{status_code} {status_message}"
|
|
147
143
|
raise ReplyError(msg, error_code=status_code)
|
|
148
144
|
|
|
149
145
|
return cls(status_code=status_code, message=status_message)
|
|
150
146
|
|
|
151
147
|
|
|
152
|
-
# noinspection PyMethodMayBeStatic
|
|
153
148
|
class Connection:
|
|
154
149
|
def send(self, request: ConnectRequest) -> bytes:
|
|
155
150
|
return request.dumps()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import enum
|
|
2
4
|
import ipaddress
|
|
3
5
|
import socket
|
|
4
6
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Optional
|
|
6
7
|
|
|
7
|
-
from .errors import ReplyError
|
|
8
8
|
from .._helpers import is_ipv4_address
|
|
9
|
+
from .errors import ReplyError
|
|
9
10
|
|
|
10
11
|
RSV = NULL = 0x00
|
|
11
12
|
SOCKS_VER = 0x04
|
|
@@ -24,14 +25,10 @@ class ReplyCode(enum.IntEnum):
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
ReplyMessages = {
|
|
27
|
-
ReplyCode.REQUEST_GRANTED:
|
|
28
|
-
ReplyCode.REQUEST_REJECTED_OR_FAILED:
|
|
29
|
-
ReplyCode.CONNECTION_FAILED:
|
|
30
|
-
|
|
31
|
-
),
|
|
32
|
-
ReplyCode.AUTHENTICATION_FAILED: (
|
|
33
|
-
'Request rejected because the client program and identd report different user-ids'
|
|
34
|
-
),
|
|
28
|
+
ReplyCode.REQUEST_GRANTED: "Request granted",
|
|
29
|
+
ReplyCode.REQUEST_REJECTED_OR_FAILED: "Request rejected or failed",
|
|
30
|
+
ReplyCode.CONNECTION_FAILED: "Request rejected because SOCKS server cannot connect to identd on the client", # noqa: E501
|
|
31
|
+
ReplyCode.AUTHENTICATION_FAILED: "Request rejected because the client program and identd report different user-ids", # noqa: E501,
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
|
|
@@ -39,10 +36,10 @@ ReplyMessages = {
|
|
|
39
36
|
class ConnectRequest:
|
|
40
37
|
host: str # hostname or IPv4 address
|
|
41
38
|
port: int
|
|
42
|
-
user_id:
|
|
39
|
+
user_id: str | None
|
|
43
40
|
|
|
44
|
-
def dumps(self):
|
|
45
|
-
port_bytes = self.port.to_bytes(2,
|
|
41
|
+
def dumps(self) -> bytes:
|
|
42
|
+
port_bytes = self.port.to_bytes(2, "big")
|
|
46
43
|
include_hostname = False
|
|
47
44
|
|
|
48
45
|
if is_ipv4_address(self.host):
|
|
@@ -56,12 +53,12 @@ class ConnectRequest:
|
|
|
56
53
|
data += host_bytes
|
|
57
54
|
|
|
58
55
|
if self.user_id:
|
|
59
|
-
data += self.user_id.encode(
|
|
56
|
+
data += self.user_id.encode("ascii")
|
|
60
57
|
|
|
61
58
|
data.append(NULL)
|
|
62
59
|
|
|
63
60
|
if include_hostname:
|
|
64
|
-
data += self.host.encode(
|
|
61
|
+
data += self.host.encode("idna")
|
|
65
62
|
data.append(NULL)
|
|
66
63
|
|
|
67
64
|
return bytes(data)
|
|
@@ -77,37 +74,36 @@ class ConnectReply:
|
|
|
77
74
|
port: int # should be ignored when using Command.CONNECT
|
|
78
75
|
|
|
79
76
|
@classmethod
|
|
80
|
-
def loads(cls, data: bytes) ->
|
|
77
|
+
def loads(cls, data: bytes) -> ConnectReply:
|
|
81
78
|
if len(data) != cls.SIZE:
|
|
82
|
-
raise ReplyError(
|
|
79
|
+
raise ReplyError("Malformed connect reply")
|
|
83
80
|
|
|
84
81
|
rsv = data[0]
|
|
85
82
|
if rsv != RSV: # pragma: no cover
|
|
86
|
-
raise ReplyError(f
|
|
83
|
+
raise ReplyError(f"Unexpected reply version: {data[0]:#02X}")
|
|
87
84
|
|
|
88
85
|
try:
|
|
89
86
|
reply = ReplyCode(data[1])
|
|
90
|
-
except ValueError:
|
|
91
|
-
raise ReplyError(f
|
|
87
|
+
except ValueError as e:
|
|
88
|
+
raise ReplyError(f"Invalid reply code: {data[1]:#02X}") from e
|
|
92
89
|
|
|
93
90
|
if reply != ReplyCode.REQUEST_GRANTED: # pragma: no cover
|
|
94
|
-
msg = ReplyMessages.get(reply,
|
|
91
|
+
msg = ReplyMessages.get(reply, "Unknown error")
|
|
95
92
|
raise ReplyError(msg, error_code=reply)
|
|
96
93
|
|
|
97
94
|
try:
|
|
98
95
|
port = int.from_bytes(data[2:4], byteorder="big")
|
|
99
|
-
except ValueError:
|
|
100
|
-
raise ReplyError(
|
|
96
|
+
except ValueError as e:
|
|
97
|
+
raise ReplyError("Invalid port data") from e
|
|
101
98
|
|
|
102
99
|
try:
|
|
103
100
|
host = socket.inet_ntop(socket.AF_INET, data[4:8])
|
|
104
|
-
except ValueError:
|
|
105
|
-
raise ReplyError(
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
raise ReplyError("Invalid port data") from e
|
|
106
103
|
|
|
107
104
|
return cls(rsv=rsv, reply=reply, host=host, port=port)
|
|
108
105
|
|
|
109
106
|
|
|
110
|
-
# noinspection PyMethodMayBeStatic
|
|
111
107
|
class Connection:
|
|
112
108
|
def send(self, request: ConnectRequest) -> bytes:
|
|
113
109
|
return request.dumps()
|