修改为东南天坐标系
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5."""
|
||||
|
||||
from .exceptions import ProtocolError, SOCKSError
|
||||
from .socks4 import (
|
||||
SOCKS4ARequest,
|
||||
SOCKS4Command,
|
||||
SOCKS4Connection,
|
||||
SOCKS4Reply,
|
||||
SOCKS4ReplyCode,
|
||||
SOCKS4Request,
|
||||
)
|
||||
from .socks5 import (
|
||||
SOCKS5AType,
|
||||
SOCKS5AuthMethod,
|
||||
SOCKS5AuthMethodsRequest,
|
||||
SOCKS5AuthReply,
|
||||
SOCKS5Command,
|
||||
SOCKS5CommandRequest,
|
||||
SOCKS5Connection,
|
||||
SOCKS5Reply,
|
||||
SOCKS5ReplyCode,
|
||||
SOCKS5UsernamePasswordRequest,
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
"SOCKS4Request",
|
||||
"SOCKS4ARequest",
|
||||
"SOCKS4Reply",
|
||||
"SOCKS4Connection",
|
||||
"SOCKS4Command",
|
||||
"SOCKS4ReplyCode",
|
||||
"SOCKS5AType",
|
||||
"SOCKS5AuthMethodsRequest",
|
||||
"SOCKS5AuthReply",
|
||||
"SOCKS5AuthMethod",
|
||||
"SOCKS5Connection",
|
||||
"SOCKS5Command",
|
||||
"SOCKS5CommandRequest",
|
||||
"SOCKS5ReplyCode",
|
||||
"SOCKS5Reply",
|
||||
"SOCKS5UsernamePasswordRequest",
|
||||
"SOCKSError",
|
||||
"ProtocolError",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
import typing
|
||||
|
||||
StrOrBytes = typing.Union[str, bytes]
|
||||
@@ -0,0 +1,86 @@
|
||||
"""Backport of @functools.singledispatchmethod to Python <3.7.
|
||||
|
||||
Adapted from https://github.com/ikalnytskyi/singledispatchmethod
|
||||
removing 2.7 specific code.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import typing
|
||||
|
||||
if hasattr(functools, "singledispatchmethod"): # pragma: nocover
|
||||
singledispatchmethod = functools.singledispatchmethod # type: ignore
|
||||
else:
|
||||
update_wrapper = functools.update_wrapper
|
||||
singledispatch = functools.singledispatch
|
||||
|
||||
# The type: ignore below is to avoid mypy erroring due to a
|
||||
# "already defined" singledispatchmethod, oddly this does not
|
||||
# happen when using `if sys.version_info >= (3, 8)`
|
||||
|
||||
class singledispatchmethod(object): # type: ignore
|
||||
"""Single-dispatch generic method descriptor.
|
||||
|
||||
TODO: Figure out how to type this:
|
||||
|
||||
`mypy --strict` returns errors like the following for all decorated methods:
|
||||
"Untyped decorator makes function "send" untyped."
|
||||
|
||||
But this is not a normal function-base decorator, it's a class and it
|
||||
doesn't have a __call__ method. When decorating the "base" method
|
||||
__init__ is called, but of course its return type is None.
|
||||
"""
|
||||
|
||||
def __init__(self, func: typing.Callable[..., typing.Any]) -> None:
|
||||
if not callable(func) and not hasattr(func, "__get__"):
|
||||
raise TypeError("{!r} is not callable or a descriptor".format(func))
|
||||
|
||||
self.dispatcher = singledispatch(func)
|
||||
self.func = func
|
||||
|
||||
def register(
|
||||
self,
|
||||
cls: typing.Callable[..., typing.Any],
|
||||
method: typing.Optional[typing.Callable[..., typing.Any]] = None,
|
||||
) -> typing.Callable[..., typing.Any]:
|
||||
"""Register a method on a class for a particular type.
|
||||
|
||||
Note in Python <= 3.6 this methods cannot infer the type from the
|
||||
argument's type annotation, users *must* supply it manually on
|
||||
decoration, i.e.
|
||||
|
||||
@my_method.register(TypeToDispatch)
|
||||
def _(self, arg: TypeToDispatch) -> None:
|
||||
...
|
||||
|
||||
Versus in Python 3.7+:
|
||||
|
||||
@my_method.register
|
||||
def _(self, arg: TypeToDispatch) -> None:
|
||||
...
|
||||
|
||||
"""
|
||||
# mypy wants method to be non-optional, but it is required to be
|
||||
# for decoration to work correctly in our case.
|
||||
# https://github.com/python/cpython/blob/3.8/Lib/functools.py#L887-L920
|
||||
# is not type annotated either.
|
||||
return self.dispatcher.register(cls, func=method) # type: ignore
|
||||
|
||||
def __get__(
|
||||
self, obj: typing.Any, cls: typing.Callable[[typing.Any], typing.Any]
|
||||
) -> typing.Callable[..., typing.Any]:
|
||||
def _method(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
||||
method = self.dispatcher.dispatch(args[0].__class__) # type: typing.Any
|
||||
return method.__get__(obj, cls)(*args, **kwargs)
|
||||
|
||||
# The type: ignore below is due to `_method` being given a strict
|
||||
# "Callable[[VarArg(Any), KwArg(Any)], Any]" which causes a
|
||||
# 'has no attribute "__isabstractmethod__" error'
|
||||
# felt safe enough to ignore
|
||||
_method.__isabstractmethod__ = self.__isabstractmethod__ # type: ignore
|
||||
_method.register = self.register # type: ignore
|
||||
update_wrapper(_method, self.func)
|
||||
return _method
|
||||
|
||||
@property
|
||||
def __isabstractmethod__(self) -> typing.Any:
|
||||
return getattr(self.func, "__isabstractmethod__", False)
|
||||
@@ -0,0 +1,6 @@
|
||||
class SOCKSError(Exception):
|
||||
"""Generic exception for when something goes wrong"""
|
||||
|
||||
|
||||
class ProtocolError(SOCKSError):
|
||||
pass
|
||||
@@ -0,0 +1,253 @@
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from ._types import StrOrBytes
|
||||
from .exceptions import ProtocolError, SOCKSError
|
||||
from .utils import (
|
||||
AddressType,
|
||||
decode_address,
|
||||
encode_address,
|
||||
get_address_port_tuple_from_address,
|
||||
)
|
||||
|
||||
|
||||
class SOCKS4ReplyCode(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS4 reply codes."""
|
||||
|
||||
REQUEST_GRANTED = b"\x5A"
|
||||
REQUEST_REJECTED_OR_FAILED = b"\x5B"
|
||||
CONNECTION_FAILED = b"\x5C"
|
||||
AUTHENTICATION_FAILED = b"\x5D"
|
||||
|
||||
|
||||
class SOCKS4Command(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS4 command codes."""
|
||||
|
||||
CONNECT = b"\x01"
|
||||
BIND = b"\x02"
|
||||
|
||||
|
||||
class SOCKS4Request(typing.NamedTuple):
|
||||
"""Encapsulates a request to the SOCKS4 proxy server
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
port: The port number to connect to on the target host.
|
||||
addr: IP address of the target host.
|
||||
user_id: Optional user ID to be included in the request, if not supplied
|
||||
the user *must* provide one in the packing operation.
|
||||
"""
|
||||
|
||||
command: SOCKS4Command
|
||||
port: int
|
||||
addr: bytes
|
||||
user_id: typing.Optional[bytes] = None
|
||||
|
||||
@classmethod
|
||||
def from_address(
|
||||
cls,
|
||||
command: SOCKS4Command,
|
||||
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
|
||||
user_id: typing.Optional[bytes] = None,
|
||||
) -> "SOCKS4Request":
|
||||
"""Convenience class method to build an instance from command and address.
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
address: A string in the form 'HOST:PORT' or a tuple of ip address string
|
||||
and port number.
|
||||
user_id: Optional user ID.
|
||||
|
||||
Returns:
|
||||
A SOCKS4Request instance.
|
||||
|
||||
Raises:
|
||||
SOCKSError: If a domain name or IPv6 address was supplied.
|
||||
"""
|
||||
address, port = get_address_port_tuple_from_address(address)
|
||||
atype, encoded_addr = encode_address(address)
|
||||
if atype != AddressType.IPV4:
|
||||
raise SOCKSError(
|
||||
"IPv6 addresses and domain names are not supported by SOCKS4"
|
||||
)
|
||||
return cls(command=command, addr=encoded_addr, port=port, user_id=user_id)
|
||||
|
||||
def dumps(self, user_id: typing.Optional[bytes] = None) -> bytes:
|
||||
"""Packs the instance into a raw binary in the appropriate form.
|
||||
|
||||
Args:
|
||||
user_id: Optional user ID as an override, if not provided the instance's
|
||||
will be used, if none was provided at initialization an error is raised.
|
||||
|
||||
Returns:
|
||||
The packed request.
|
||||
|
||||
Raises:
|
||||
SOCKSError: If no user was specified in this call or on initialization.
|
||||
"""
|
||||
user_id = user_id or self.user_id
|
||||
if user_id is None:
|
||||
raise SOCKSError("SOCKS4 requires a user_id, none was specified")
|
||||
|
||||
return b"".join(
|
||||
[
|
||||
b"\x04",
|
||||
self.command,
|
||||
(self.port).to_bytes(2, byteorder="big"),
|
||||
self.addr,
|
||||
user_id,
|
||||
b"\x00",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SOCKS4ARequest(typing.NamedTuple):
|
||||
"""Encapsulates a request to the SOCKS4A proxy server
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
port: The port number to connect to on the target host.
|
||||
addr: IP address of the target host.
|
||||
user_id: Optional user ID to be included in the request, if not supplied
|
||||
the user *must* provide one in the packing operation.
|
||||
"""
|
||||
|
||||
command: SOCKS4Command
|
||||
port: int
|
||||
addr: bytes
|
||||
user_id: typing.Optional[bytes] = None
|
||||
|
||||
@classmethod
|
||||
def from_address(
|
||||
cls,
|
||||
command: SOCKS4Command,
|
||||
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
|
||||
user_id: typing.Optional[bytes] = None,
|
||||
) -> "SOCKS4ARequest":
|
||||
"""Convenience class method to build an instance from command and address.
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
address: A string in the form 'HOST:PORT' or a tuple of ip address string
|
||||
and port number.
|
||||
user_id: Optional user ID.
|
||||
|
||||
Returns:
|
||||
A SOCKS4ARequest instance.
|
||||
"""
|
||||
address, port = get_address_port_tuple_from_address(address)
|
||||
atype, encoded_addr = encode_address(address)
|
||||
return cls(command=command, addr=encoded_addr, port=port, user_id=user_id)
|
||||
|
||||
def dumps(self, user_id: typing.Optional[bytes] = None) -> bytes:
|
||||
"""Packs the instance into a raw binary in the appropriate form.
|
||||
|
||||
Args:
|
||||
user_id: Optional user ID as an override, if not provided the instance's
|
||||
will be used, if none was provided at initialization an error is raised.
|
||||
|
||||
Returns:
|
||||
The packed request.
|
||||
|
||||
Raises:
|
||||
SOCKSError: If no user was specified in this call or on initialization.
|
||||
"""
|
||||
user_id = user_id or self.user_id
|
||||
if user_id is None:
|
||||
raise SOCKSError("SOCKS4 requires a user_id, none was specified")
|
||||
|
||||
return b"".join(
|
||||
[
|
||||
b"\x04",
|
||||
self.command,
|
||||
(self.port).to_bytes(2, byteorder="big"),
|
||||
b"\x00\x00\x00\xFF", # arbitrary final non-zero byte
|
||||
user_id,
|
||||
b"\x00",
|
||||
self.addr,
|
||||
b"\x00",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SOCKS4Reply(typing.NamedTuple):
|
||||
"""Encapsulates a reply from the SOCKS4 proxy server
|
||||
|
||||
Args:
|
||||
reply_code: The code representing the type of reply.
|
||||
port: The port number returned.
|
||||
addr: Optional IP address returned.
|
||||
"""
|
||||
|
||||
reply_code: SOCKS4ReplyCode
|
||||
port: int
|
||||
addr: typing.Optional[str]
|
||||
|
||||
@classmethod
|
||||
def loads(cls, data: bytes) -> "SOCKS4Reply":
|
||||
"""Unpacks the reply data into an instance.
|
||||
|
||||
Returns:
|
||||
The unpacked reply instance.
|
||||
|
||||
Raises:
|
||||
ProtocolError: If the data does not match the spec.
|
||||
"""
|
||||
if len(data) != 8 or data[0:1] != b"\x00":
|
||||
raise ProtocolError("Malformed reply")
|
||||
|
||||
try:
|
||||
return cls(
|
||||
reply_code=SOCKS4ReplyCode(data[1:2]),
|
||||
port=int.from_bytes(data[2:4], byteorder="big"),
|
||||
addr=decode_address(AddressType.IPV4, data[4:8]),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ProtocolError("Malformed reply") from exc
|
||||
|
||||
|
||||
class SOCKS4Connection:
|
||||
"""Encapsulates a SOCKS4 and SOCKS4A connection.
|
||||
|
||||
Packs request objects into data suitable to be send and unpacks reply
|
||||
data into their appropriate reply objects.
|
||||
|
||||
Args:
|
||||
user_id: The user ID to be sent as part of the requests.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: bytes):
|
||||
self.user_id = user_id
|
||||
|
||||
self._data_to_send = bytearray()
|
||||
self._received_data = bytearray()
|
||||
|
||||
def send(self, request: typing.Union[SOCKS4Request, SOCKS4ARequest]) -> None:
|
||||
"""Packs a request object and adds it to the send data buffer.
|
||||
|
||||
Args:
|
||||
request: The request instance to be packed.
|
||||
"""
|
||||
user_id = request.user_id or self.user_id
|
||||
self._data_to_send += request.dumps(user_id=user_id)
|
||||
|
||||
def receive_data(self, data: bytes) -> SOCKS4Reply:
|
||||
"""Unpacks response data into a reply object.
|
||||
|
||||
Args:
|
||||
data: The raw response data from the proxy server.
|
||||
|
||||
Returns:
|
||||
The appropriate reply object.
|
||||
"""
|
||||
self._received_data += data
|
||||
return SOCKS4Reply.loads(bytes(self._received_data))
|
||||
|
||||
def data_to_send(self) -> bytes:
|
||||
"""Returns the data to be sent via the I/O library of choice.
|
||||
|
||||
Also clears the connection's buffer.
|
||||
"""
|
||||
data = bytes(self._data_to_send)
|
||||
self._data_to_send = bytearray()
|
||||
return data
|
||||
@@ -0,0 +1,399 @@
|
||||
import enum
|
||||
import typing
|
||||
|
||||
from ._types import StrOrBytes
|
||||
from .compat import singledispatchmethod
|
||||
from .exceptions import ProtocolError
|
||||
from .utils import (
|
||||
AddressType,
|
||||
decode_address,
|
||||
encode_address,
|
||||
get_address_port_tuple_from_address,
|
||||
)
|
||||
|
||||
|
||||
class SOCKS5AuthMethod(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS5 authentication methods."""
|
||||
|
||||
NO_AUTH_REQUIRED = b"\x00"
|
||||
GSSAPI = b"\x01"
|
||||
USERNAME_PASSWORD = b"\x02"
|
||||
NO_ACCEPTABLE_METHODS = b"\xFF"
|
||||
|
||||
|
||||
class SOCKS5Command(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS5 commands."""
|
||||
|
||||
CONNECT = b"\x01"
|
||||
BIND = b"\x02"
|
||||
UDP_ASSOCIATE = b"\x03"
|
||||
|
||||
|
||||
class SOCKS5AType(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS5 address types."""
|
||||
|
||||
IPV4_ADDRESS = b"\x01"
|
||||
DOMAIN_NAME = b"\x03"
|
||||
IPV6_ADDRESS = b"\x04"
|
||||
|
||||
@classmethod
|
||||
def from_atype(cls, atype: AddressType) -> "SOCKS5AType":
|
||||
if atype == AddressType.IPV4:
|
||||
return SOCKS5AType.IPV4_ADDRESS
|
||||
elif atype == AddressType.DN:
|
||||
return SOCKS5AType.DOMAIN_NAME
|
||||
elif atype == AddressType.IPV6:
|
||||
return SOCKS5AType.IPV6_ADDRESS
|
||||
raise ValueError(atype)
|
||||
|
||||
|
||||
class SOCKS5ReplyCode(bytes, enum.Enum):
|
||||
"""Enumeration of SOCKS5 reply codes."""
|
||||
|
||||
SUCCEEDED = b"\x00"
|
||||
GENERAL_SERVER_FAILURE = b"\x01"
|
||||
CONNECTION_NOT_ALLOWED_BY_RULESET = b"\x02"
|
||||
NETWORK_UNREACHABLE = b"\x03"
|
||||
HOST_UNREACHABLE = b"\x04"
|
||||
CONNECTION_REFUSED = b"\x05"
|
||||
TTL_EXPIRED = b"\x06"
|
||||
COMMAND_NOT_SUPPORTED = b"\x07"
|
||||
ADDRESS_TYPE_NOT_SUPPORTED = b"\x08"
|
||||
|
||||
|
||||
class SOCKS5AuthMethodsRequest(typing.NamedTuple):
|
||||
"""Encapsulates a request to the proxy for available authentication methods.
|
||||
|
||||
Args:
|
||||
methods: A list of acceptable authentication methods.
|
||||
"""
|
||||
|
||||
methods: typing.List[SOCKS5AuthMethod]
|
||||
|
||||
def dumps(self) -> bytes:
|
||||
"""Packs the instance into a raw binary in the appropriate form."""
|
||||
|
||||
return b"".join(
|
||||
[
|
||||
b"\x05",
|
||||
len(self.methods).to_bytes(1, byteorder="big"),
|
||||
b"".join(self.methods),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SOCKS5AuthReply(typing.NamedTuple):
|
||||
"""Encapsulates a reply from the proxy with the authentication method to be used.
|
||||
|
||||
Args:
|
||||
method: The authentication method to be used.
|
||||
|
||||
Raises:
|
||||
ProtocolError: If the data does not conform with the expected structure.
|
||||
"""
|
||||
|
||||
method: SOCKS5AuthMethod
|
||||
|
||||
@classmethod
|
||||
def loads(cls, data: bytes) -> "SOCKS5AuthReply":
|
||||
"""Unpacks the authentication reply data into an instance.
|
||||
|
||||
Returns:
|
||||
The unpacked authentication reply instance.
|
||||
|
||||
Raises:
|
||||
ProtocolError: If the data does not match the spec.
|
||||
"""
|
||||
if len(data) != 2:
|
||||
raise ProtocolError("Malformed reply")
|
||||
|
||||
try:
|
||||
return cls(method=SOCKS5AuthMethod(data[1:2]))
|
||||
except ValueError as exc:
|
||||
raise ProtocolError("Malformed reply") from exc
|
||||
|
||||
|
||||
class SOCKS5UsernamePasswordRequest(typing.NamedTuple):
|
||||
"""Encapsulates a username/password authentication request to the proxy server."""
|
||||
|
||||
username: bytes
|
||||
password: bytes
|
||||
|
||||
def dumps(self) -> bytes:
|
||||
"""Packs the instance into a raw binary in the appropriate form.
|
||||
|
||||
Returns:
|
||||
The packed request.
|
||||
"""
|
||||
return b"".join(
|
||||
[
|
||||
b"\x01",
|
||||
len(self.username).to_bytes(1, byteorder="big"),
|
||||
self.username,
|
||||
len(self.password).to_bytes(1, byteorder="big"),
|
||||
self.password,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SOCKS5UsernamePasswordReply(typing.NamedTuple):
|
||||
"""Encapsulates a username/password authentication reply from the proxy server."""
|
||||
|
||||
success: bool
|
||||
|
||||
@classmethod
|
||||
def loads(cls, data: bytes) -> "SOCKS5UsernamePasswordReply":
|
||||
"""Unpacks the reply authentication data into an instance.
|
||||
|
||||
Returns:
|
||||
The unpacked authentication reply instance.
|
||||
"""
|
||||
return cls(success=data == b"\x01\x00")
|
||||
|
||||
|
||||
class SOCKS5CommandRequest(typing.NamedTuple):
|
||||
"""Encapsulates a command request to the proxy server.
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
atype: The address type of the addr field.
|
||||
addr: Address of the target host.
|
||||
port: The port number to connect to on the target host.
|
||||
"""
|
||||
|
||||
command: SOCKS5Command
|
||||
atype: SOCKS5AType
|
||||
addr: bytes
|
||||
port: int
|
||||
|
||||
@classmethod
|
||||
def from_address(
|
||||
cls,
|
||||
command: SOCKS5Command,
|
||||
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]],
|
||||
) -> "SOCKS5CommandRequest":
|
||||
"""Convenience class method to build an instance from command and address.
|
||||
|
||||
Args:
|
||||
command: The command to request.
|
||||
address: A string in the form 'HOST:PORT' or a tuple of ip address string
|
||||
and port number. The address type will be inferred.
|
||||
|
||||
Returns:
|
||||
A SOCKS5CommandRequest instance.
|
||||
|
||||
Raises:
|
||||
SOCKSError: If a domain name or IPv6 address was supplied.
|
||||
"""
|
||||
address, port = get_address_port_tuple_from_address(address)
|
||||
atype, encoded_addr = encode_address(address)
|
||||
return cls(
|
||||
command=command,
|
||||
atype=SOCKS5AType.from_atype(atype),
|
||||
addr=encoded_addr,
|
||||
port=port,
|
||||
)
|
||||
|
||||
def dumps(self) -> bytes:
|
||||
"""Packs the instance into a raw binary in the appropriate form.
|
||||
|
||||
Returns:
|
||||
The packed request.
|
||||
"""
|
||||
return b"".join(
|
||||
[
|
||||
b"\x05",
|
||||
self.command,
|
||||
b"\x00",
|
||||
self.atype,
|
||||
self.packed_addr,
|
||||
(self.port).to_bytes(2, byteorder="big"),
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def packed_addr(self) -> bytes:
|
||||
"""Property returning the packed address in the correct form for its type."""
|
||||
if self.atype == SOCKS5AType.IPV4_ADDRESS:
|
||||
assert len(self.addr) == 4
|
||||
return self.addr
|
||||
elif self.atype == SOCKS5AType.IPV6_ADDRESS:
|
||||
assert len(self.addr) == 16
|
||||
return self.addr
|
||||
else:
|
||||
length = len(self.addr)
|
||||
return length.to_bytes(1, byteorder="big") + self.addr
|
||||
|
||||
|
||||
class SOCKS5Reply(typing.NamedTuple):
|
||||
"""Encapsulates a reply from the SOCKS5 proxy server
|
||||
|
||||
Args:
|
||||
reply_code: The code representing the type of reply.
|
||||
atype: The address type of the addr field.
|
||||
addr: Optional IP address returned.
|
||||
port: The port number returned.
|
||||
"""
|
||||
|
||||
reply_code: SOCKS5ReplyCode
|
||||
atype: SOCKS5AType
|
||||
addr: str
|
||||
port: int
|
||||
|
||||
@classmethod
|
||||
def loads(cls, data: bytes) -> "SOCKS5Reply":
|
||||
"""Unpacks the reply data into an instance.
|
||||
|
||||
Returns:
|
||||
The unpacked reply instance.
|
||||
|
||||
Raises:
|
||||
ProtocolError: If the data does not match the spec.
|
||||
"""
|
||||
if data[0:1] != b"\x05":
|
||||
raise ProtocolError("Malformed reply")
|
||||
|
||||
try:
|
||||
atype = SOCKS5AType(data[3:4])
|
||||
|
||||
return cls(
|
||||
reply_code=SOCKS5ReplyCode(data[1:2]),
|
||||
atype=atype,
|
||||
addr=decode_address(AddressType.from_socks5_atype(atype), data[4:-2]),
|
||||
port=int.from_bytes(data[-2:], byteorder="big"),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ProtocolError("Malformed reply") from exc
|
||||
|
||||
|
||||
class SOCKS5Datagram(typing.NamedTuple):
|
||||
"""Encapsulates a SOCKS5 datagram for UDP connections.
|
||||
|
||||
Currently not implemented.
|
||||
"""
|
||||
|
||||
atype: SOCKS5AType
|
||||
addr: bytes
|
||||
port: int
|
||||
data: bytes
|
||||
|
||||
fragment: int
|
||||
last_fragment: bool
|
||||
|
||||
@classmethod
|
||||
def loads(cls, data: bytes) -> "SOCKS5Datagram":
|
||||
raise NotImplementedError() # pragma: nocover
|
||||
|
||||
def dumps(self) -> bytes:
|
||||
raise NotImplementedError() # pragma: nocover
|
||||
|
||||
|
||||
class SOCKS5State(enum.IntEnum):
|
||||
"""Enumeration of SOCKS5 protocol states."""
|
||||
|
||||
CLIENT_AUTH_REQUIRED = 1
|
||||
SERVER_AUTH_REPLY = 2
|
||||
CLIENT_AUTHENTICATED = 3
|
||||
TUNNEL_READY = 4
|
||||
CLIENT_WAITING_FOR_USERNAME_PASSWORD = 5
|
||||
SERVER_VERIFY_USERNAME_PASSWORD = 6
|
||||
MUST_CLOSE = 7
|
||||
|
||||
|
||||
SOCKS5RequestType = typing.Union[SOCKS5AuthMethodsRequest, SOCKS5CommandRequest]
|
||||
|
||||
|
||||
class SOCKS5Connection:
|
||||
"""Encapsulates a SOCKS5 connection.
|
||||
|
||||
Packs request objects into data suitable to be send and unpacks reply
|
||||
data into their appropriate reply objects.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._data_to_send = bytearray()
|
||||
self._received_data = bytearray()
|
||||
self._state = SOCKS5State.CLIENT_AUTH_REQUIRED
|
||||
|
||||
@property
|
||||
def state(self) -> SOCKS5State:
|
||||
"""Returns the current state of the protocol."""
|
||||
return self._state
|
||||
|
||||
@singledispatchmethod # type: ignore
|
||||
def send(self, request: SOCKS5RequestType) -> None:
|
||||
"""Packs a request object and adds it to the send data buffer.
|
||||
|
||||
Also progresses the protocol state of the connection.
|
||||
|
||||
Args:
|
||||
request: The request instance to be packed.
|
||||
"""
|
||||
raise NotImplementedError() # pragma: nocover
|
||||
|
||||
@send.register(SOCKS5AuthMethodsRequest) # type: ignore
|
||||
def _auth_methods(self, request: SOCKS5AuthMethodsRequest) -> None:
|
||||
self._data_to_send += request.dumps()
|
||||
self._state = SOCKS5State.SERVER_AUTH_REPLY
|
||||
|
||||
@send.register(SOCKS5UsernamePasswordRequest) # type: ignore
|
||||
def _auth_username_password(self, request: SOCKS5UsernamePasswordRequest) -> None:
|
||||
if self._state != SOCKS5State.CLIENT_WAITING_FOR_USERNAME_PASSWORD:
|
||||
raise ProtocolError("Not currently waiting for username and password")
|
||||
self._state = SOCKS5State.SERVER_VERIFY_USERNAME_PASSWORD
|
||||
self._data_to_send += request.dumps()
|
||||
|
||||
@send.register(SOCKS5CommandRequest) # type: ignore
|
||||
def _command(self, request: SOCKS5AuthMethodsRequest) -> None:
|
||||
if self._state < SOCKS5State.CLIENT_AUTHENTICATED:
|
||||
raise ProtocolError(
|
||||
"SOCKS5 connections must be authenticated before sending a request"
|
||||
)
|
||||
self._data_to_send += request.dumps()
|
||||
|
||||
def receive_data(
|
||||
self, data: bytes
|
||||
) -> typing.Union[SOCKS5AuthReply, SOCKS5Reply, SOCKS5UsernamePasswordReply]:
|
||||
"""Unpacks response data into a reply object.
|
||||
|
||||
Args:
|
||||
data: The raw response data from the proxy server.
|
||||
|
||||
Returns:
|
||||
A reply instance corresponding to the connection state and reply data.
|
||||
"""
|
||||
if self._state == SOCKS5State.SERVER_AUTH_REPLY:
|
||||
auth_reply = SOCKS5AuthReply.loads(data)
|
||||
if auth_reply.method == SOCKS5AuthMethod.USERNAME_PASSWORD:
|
||||
self._state = SOCKS5State.CLIENT_WAITING_FOR_USERNAME_PASSWORD
|
||||
elif auth_reply.method == SOCKS5AuthMethod.NO_AUTH_REQUIRED:
|
||||
self._state = SOCKS5State.CLIENT_AUTHENTICATED
|
||||
return auth_reply
|
||||
|
||||
if self._state == SOCKS5State.SERVER_VERIFY_USERNAME_PASSWORD:
|
||||
username_password_reply = SOCKS5UsernamePasswordReply.loads(data)
|
||||
if username_password_reply.success:
|
||||
self._state = SOCKS5State.CLIENT_AUTHENTICATED
|
||||
else:
|
||||
self._state = SOCKS5State.MUST_CLOSE
|
||||
return username_password_reply
|
||||
|
||||
if self._state == SOCKS5State.CLIENT_AUTHENTICATED:
|
||||
reply = SOCKS5Reply.loads(data)
|
||||
if reply.reply_code == SOCKS5ReplyCode.SUCCEEDED:
|
||||
self._state = SOCKS5State.TUNNEL_READY
|
||||
else:
|
||||
self._state = SOCKS5State.MUST_CLOSE
|
||||
|
||||
return reply
|
||||
|
||||
raise NotImplementedError() # pragma: nocover
|
||||
|
||||
def data_to_send(self) -> bytes:
|
||||
"""Returns the data to be sent via the I/O library of choice.
|
||||
|
||||
Also clears the connection's buffer.
|
||||
"""
|
||||
data = bytes(self._data_to_send)
|
||||
self._data_to_send = bytearray()
|
||||
return data
|
||||
@@ -0,0 +1,95 @@
|
||||
import enum
|
||||
import functools
|
||||
import re
|
||||
import socket
|
||||
import typing
|
||||
|
||||
from ._types import StrOrBytes
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from socksio.socks5 import SOCKS5AType # pragma: nocover
|
||||
|
||||
|
||||
IP_V6_WITH_PORT_REGEX = re.compile(r"^\[(?P<address>[^\]]+)\]:(?P<port>\d+)$")
|
||||
|
||||
|
||||
class AddressType(enum.Enum):
|
||||
IPV4 = "IPV4"
|
||||
IPV6 = "IPV6"
|
||||
DN = "DN"
|
||||
|
||||
@classmethod
|
||||
def from_socks5_atype(cls, socks5atype: "SOCKS5AType") -> "AddressType":
|
||||
from socksio.socks5 import SOCKS5AType
|
||||
|
||||
if socks5atype == SOCKS5AType.IPV4_ADDRESS:
|
||||
return AddressType.IPV4
|
||||
elif socks5atype == SOCKS5AType.DOMAIN_NAME:
|
||||
return AddressType.DN
|
||||
elif socks5atype == SOCKS5AType.IPV6_ADDRESS:
|
||||
return AddressType.IPV6
|
||||
raise ValueError(socks5atype)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=64)
|
||||
def encode_address(addr: StrOrBytes) -> typing.Tuple[AddressType, bytes]:
|
||||
"""Determines the type of address and encodes it into the format SOCKS expects"""
|
||||
addr = addr.decode() if isinstance(addr, bytes) else addr
|
||||
try:
|
||||
return AddressType.IPV6, socket.inet_pton(socket.AF_INET6, addr)
|
||||
except OSError:
|
||||
try:
|
||||
return AddressType.IPV4, socket.inet_pton(socket.AF_INET, addr)
|
||||
except OSError:
|
||||
return AddressType.DN, addr.encode()
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=64)
|
||||
def decode_address(address_type: AddressType, encoded_addr: bytes) -> str:
|
||||
"""Decodes the address from a SOCKS reply"""
|
||||
if address_type == AddressType.IPV6:
|
||||
return socket.inet_ntop(socket.AF_INET6, encoded_addr)
|
||||
elif address_type == AddressType.IPV4:
|
||||
return socket.inet_ntop(socket.AF_INET, encoded_addr)
|
||||
else:
|
||||
assert address_type == AddressType.DN
|
||||
return encoded_addr.decode()
|
||||
|
||||
|
||||
def split_address_port_from_string(address: StrOrBytes) -> typing.Tuple[str, int]:
|
||||
"""Returns a tuple (address: str, port: int) from an address string with a port
|
||||
i.e. '127.0.0.1:8080', '[0:0:0:0:0:0:0:1]:3080' or 'localhost:8080'.
|
||||
|
||||
Note no validation is done on the domain or IP itself.
|
||||
"""
|
||||
address = address.decode() if isinstance(address, bytes) else address
|
||||
match = re.match(IP_V6_WITH_PORT_REGEX, address)
|
||||
if match:
|
||||
address, str_port = match.group("address"), match.group("port")
|
||||
else:
|
||||
address, _, str_port = address.partition(":")
|
||||
|
||||
try:
|
||||
return address, int(str_port)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"Invalid address + port. Please supply a valid domain name, IPV4 or IPV6 "
|
||||
"address with the port as a suffix, i.e. `127.0.0.1:3080`, "
|
||||
"`[0:0:0:0:0:0:0:1]:3080` or `localhost:3080`"
|
||||
) from None
|
||||
|
||||
|
||||
def get_address_port_tuple_from_address(
|
||||
address: typing.Union[StrOrBytes, typing.Tuple[StrOrBytes, int]]
|
||||
) -> typing.Tuple[str, int]:
|
||||
"""Returns an (address, port) from an address string-like or tuple."""
|
||||
if isinstance(address, tuple):
|
||||
address, port = address
|
||||
if isinstance(address, bytes):
|
||||
address = address.decode()
|
||||
if isinstance(port, (str, bytes)):
|
||||
port = int(port)
|
||||
else:
|
||||
address, port = split_address_port_from_string(address)
|
||||
|
||||
return address, port
|
||||
Reference in New Issue
Block a user