├── python_socks ├── py.typed ├── _connectors │ ├── __init__.py │ ├── abc.py │ ├── http_sync.py │ ├── http_async.py │ ├── factory_sync.py │ ├── factory_async.py │ ├── socks4_sync.py │ ├── socks4_async.py │ ├── socks5_sync.py │ └── socks5_async.py ├── _protocols │ ├── __init__.py │ ├── errors.py │ ├── socks4.py │ └── http.py ├── _version.py ├── async_ │ ├── __init__.py │ ├── trio │ │ ├── __init__.py │ │ ├── v2 │ │ │ ├── __init__.py │ │ │ ├── _connect.py │ │ │ ├── _chain.py │ │ │ ├── _stream.py │ │ │ └── _proxy.py │ │ ├── _resolver.py │ │ ├── _connect.py │ │ ├── _stream.py │ │ └── _proxy.py │ ├── curio │ │ ├── __init__.py │ │ ├── _connect.py │ │ ├── _resolver.py │ │ ├── _stream.py │ │ └── _proxy.py │ ├── asyncio │ │ ├── __init__.py │ │ ├── v2 │ │ │ ├── __init__.py │ │ │ ├── _connect.py │ │ │ ├── _chain.py │ │ │ ├── _stream.py │ │ │ └── _proxy.py │ │ ├── _resolver.py │ │ ├── _stream.py │ │ ├── _connect.py │ │ └── _proxy.py │ ├── anyio │ │ ├── __init__.py │ │ ├── v2 │ │ │ ├── __init__.py │ │ │ ├── _connect.py │ │ │ ├── _chain.py │ │ │ ├── _stream.py │ │ │ └── _proxy.py │ │ ├── _connect.py │ │ ├── _resolver.py │ │ ├── _chain.py │ │ ├── _stream.py │ │ └── _proxy.py │ └── _proxy_chain.py ├── _types.py ├── sync │ ├── __init__.py │ ├── v2 │ │ ├── __init__.py │ │ ├── _connect.py │ │ ├── _chain.py │ │ ├── _stream.py │ │ ├── _proxy.py │ │ └── _ssl_transport.py │ ├── _connect.py │ ├── _resolver.py │ ├── _stream.py │ ├── _chain.py │ └── _proxy.py ├── _errors.py ├── __init__.py ├── _abc.py └── _helpers.py ├── tests ├── __init__.py ├── test_misc.py ├── http_app.py ├── utils.py ├── http_server.py ├── mocks.py ├── config.py ├── test_resolvers.py ├── proxy_server.py ├── test_proxy_async_aio.py ├── test_proxy_async_trio.py ├── test_proxy_sync.py ├── test_proxy_async_aio_v2.py ├── test_proxy_async_anyio.py ├── test_proxy_async_trio_v2.py ├── test_proxy_async_anyio_v2.py ├── test_proxy_sync_v2.py ├── conftest.py └── test_proxy_async_curio.py ├── .flake8 ├── MANIFEST.in ├── requirements-dev.txt ├── .coveragerc ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── pyproject.toml └── README.md /python_socks/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_socks/_connectors/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /python_socks/_protocols/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = N805,W503 3 | max-line-length = 99 4 | -------------------------------------------------------------------------------- /python_socks/_version.py: -------------------------------------------------------------------------------- 1 | __title__ = 'python-socks' 2 | __version__ = '2.8.0' 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | recursive-include tests * 4 | -------------------------------------------------------------------------------- /python_socks/async_/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy_chain import ProxyChain 2 | 3 | __all__ = ('ProxyChain',) 4 | -------------------------------------------------------------------------------- /python_socks/async_/trio/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import TrioProxy as Proxy 2 | 3 | __all__ = ('Proxy',) 4 | -------------------------------------------------------------------------------- /python_socks/async_/curio/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import CurioProxy as Proxy 2 | 3 | 4 | __all__ = ('Proxy',) 5 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import AsyncioProxy as Proxy 2 | 3 | 4 | __all__ = ('Proxy',) 5 | -------------------------------------------------------------------------------- /python_socks/_types.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ProxyType(Enum): 5 | SOCKS4 = 1 6 | SOCKS5 = 2 7 | HTTP = 3 8 | -------------------------------------------------------------------------------- /python_socks/sync/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import SyncProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | 5 | __all__ = ('Proxy', 'ProxyChain') 6 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import AnyioProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | __all__ = ('Proxy', 'ProxyChain') 5 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import AsyncioProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | __all__ = ('Proxy', 'ProxyChain') 5 | -------------------------------------------------------------------------------- /python_socks/sync/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import SyncProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | __all__ = ( 5 | 'Proxy', 6 | 'ProxyChain', 7 | ) 8 | -------------------------------------------------------------------------------- /python_socks/async_/trio/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import TrioProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | __all__ = ( 5 | 'Proxy', 6 | 'ProxyChain', 7 | ) 8 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/v2/__init__.py: -------------------------------------------------------------------------------- 1 | from ._proxy import AnyioProxy as Proxy 2 | from ._chain import ProxyChain 3 | 4 | __all__ = ( 5 | 'Proxy', 6 | 'ProxyChain', 7 | ) 8 | -------------------------------------------------------------------------------- /python_socks/_protocols/errors.py: -------------------------------------------------------------------------------- 1 | class ReplyError(Exception): 2 | def __init__(self, message, error_code=None): 3 | super().__init__(message) 4 | self.error_code = error_code 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.9.1 2 | pytest>=8.3.4 3 | pytest-cov>=5.0.0 4 | pytest-asyncio>=0.24.0 5 | trustme>=0.9.0 6 | trio>=0.24.0 7 | pytest-trio>=0.8.0 8 | attrs>=22.1.0 9 | curio>=1.4; python_version < "3.12" # https://github.com/dabeaz/curio/issues/367 10 | anyio>=3.3.4,<5.0.0 11 | yarl>=1.4.2 12 | async-timeout>=4.0.0; python_version < "3.11" 13 | flask>=1.1.2 14 | tiny-proxy>=0.1.1 15 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/_connect.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import anyio 3 | import anyio.abc 4 | 5 | 6 | async def connect_tcp( 7 | host: str, 8 | port: int, 9 | local_host: Optional[str] = None, 10 | ) -> anyio.abc.SocketStream: 11 | 12 | return await anyio.connect_tcp( 13 | remote_host=host, 14 | remote_port=port, 15 | local_host=local_host, 16 | ) 17 | -------------------------------------------------------------------------------- /python_socks/_errors.py: -------------------------------------------------------------------------------- 1 | class ProxyException(Exception): 2 | pass 3 | 4 | 5 | class ProxyTimeoutError(ProxyException, TimeoutError): 6 | pass 7 | 8 | 9 | class ProxyConnectionError(ProxyException, OSError): 10 | pass 11 | 12 | 13 | class ProxyError(ProxyException): 14 | def __init__(self, message, error_code=None): 15 | super().__init__(message) 16 | self.error_code = error_code 17 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # */_proxy_chain_*.py 4 | # python_socks/async_/asyncio/ext/* 5 | python_socks/_basic_auth.py 6 | python_socks/sync/v2/_ssl_transport.py 7 | [report] 8 | # Regexes for lines to exclude from consideration 9 | exclude_lines = 10 | pragma: no cover 11 | pragma: nocover 12 | def __repr__ 13 | if self.debug: 14 | raise NotImplementedError 15 | raise ValueError 16 | -------------------------------------------------------------------------------- /python_socks/async_/curio/_connect.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | import curio 4 | import curio.io 5 | import curio.socket 6 | 7 | 8 | async def connect_tcp( 9 | host: str, 10 | port: int, 11 | local_addr: Optional[Tuple[str, int]] = None, 12 | ) -> curio.io.Socket: 13 | return await curio.open_connection( 14 | host=host, 15 | port=port, 16 | source_addr=local_addr, 17 | ) 18 | -------------------------------------------------------------------------------- /python_socks/sync/_connect.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional, Tuple 3 | 4 | 5 | def connect_tcp( 6 | host: str, 7 | port: int, 8 | timeout: Optional[float] = None, 9 | local_addr: Optional[Tuple[str, int]] = None, 10 | ) -> socket.socket: 11 | address = (host, port) 12 | return socket.create_connection( 13 | address, 14 | timeout, 15 | source_address=local_addr, 16 | ) 17 | -------------------------------------------------------------------------------- /python_socks/async_/trio/v2/_connect.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import trio 4 | from ._stream import TrioSocketStream 5 | 6 | 7 | async def connect_tcp( 8 | host: str, 9 | port: int, 10 | local_addr: Optional[str] = None, 11 | ) -> TrioSocketStream: 12 | trio_stream = await trio.open_tcp_stream( 13 | host=host, 14 | port=port, 15 | local_address=local_addr, 16 | ) 17 | return TrioSocketStream(trio_stream) 18 | -------------------------------------------------------------------------------- /python_socks/__init__.py: -------------------------------------------------------------------------------- 1 | from ._version import __version__, __title__ 2 | 3 | from ._types import ProxyType 4 | from ._helpers import parse_proxy_url 5 | 6 | from ._errors import ( 7 | ProxyError, 8 | ProxyTimeoutError, 9 | ProxyConnectionError, 10 | ) 11 | 12 | __all__ = ( 13 | '__title__', 14 | '__version__', 15 | 'ProxyError', 16 | 'ProxyTimeoutError', 17 | 'ProxyConnectionError', 18 | 'ProxyType', 19 | 'parse_proxy_url', 20 | ) 21 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/v2/_connect.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import anyio 3 | import anyio.abc 4 | from ._stream import AnyioSocketStream 5 | 6 | 7 | async def connect_tcp( 8 | host: str, 9 | port: int, 10 | local_host: Optional[str] = None, 11 | ) -> AnyioSocketStream: 12 | s = await anyio.connect_tcp( 13 | remote_host=host, 14 | remote_port=port, 15 | local_host=local_host, 16 | ) 17 | return AnyioSocketStream(s) 18 | -------------------------------------------------------------------------------- /python_socks/_connectors/abc.py: -------------------------------------------------------------------------------- 1 | from .._abc import SyncSocketStream, AsyncSocketStream 2 | 3 | 4 | class SyncConnector: 5 | def connect( 6 | self, 7 | stream: SyncSocketStream, 8 | host: str, 9 | port: int, 10 | ): 11 | raise NotImplementedError 12 | 13 | 14 | class AsyncConnector: 15 | async def connect( 16 | self, 17 | stream: AsyncSocketStream, 18 | host: str, 19 | port: int, 20 | ): 21 | raise NotImplementedError 22 | -------------------------------------------------------------------------------- /python_socks/sync/v2/_connect.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional, Tuple 3 | from ._stream import SyncSocketStream 4 | 5 | 6 | def connect_tcp( 7 | host: str, 8 | port: int, 9 | timeout: Optional[float] = None, 10 | local_addr: Optional[Tuple[str, int]] = None, 11 | ) -> SyncSocketStream: 12 | address = (host, port) 13 | sock = socket.create_connection( 14 | address, 15 | timeout, 16 | source_address=local_addr, 17 | ) 18 | 19 | return SyncSocketStream(sock) 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.egg 3 | *.egg-info 4 | *.eggs 5 | *.pyc 6 | *.pyd 7 | *.pyo 8 | *.so 9 | *.tar.gz 10 | *~ 11 | .DS_Store 12 | .Python 13 | .cache 14 | .coverage 15 | .coverage.* 16 | .idea 17 | .installed.cfg 18 | .noseids 19 | .tox 20 | .vimrc 21 | # bin 22 | build 23 | cover 24 | coverage 25 | develop-eggs 26 | dist 27 | docs/_build/ 28 | eggs 29 | include/ 30 | lib/ 31 | man/ 32 | nosetests.xml 33 | parts 34 | pyvenv 35 | sources 36 | var/* 37 | venv 38 | virtualenv.py 39 | .install-deps 40 | .develop 41 | .idea/ 42 | .vscode/ 43 | usage*.py -------------------------------------------------------------------------------- /python_socks/sync/_resolver.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from .. import _abc as abc 3 | 4 | 5 | class SyncResolver(abc.SyncResolver): 6 | # noinspection PyMethodMayBeStatic 7 | def resolve(self, host, port=0, family=socket.AF_UNSPEC): 8 | infos = socket.getaddrinfo(host=host, port=port, family=family, type=socket.SOCK_STREAM) 9 | 10 | if not infos: # pragma: no cover 11 | raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family)) 12 | 13 | infos = sorted(infos, key=lambda info: info[0]) 14 | 15 | family, _, _, _, address = infos[0] 16 | return family, address[0] 17 | -------------------------------------------------------------------------------- /tests/test_misc.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPackageRequirements 2 | import pytest 3 | 4 | from python_socks._helpers import is_ip_address # noqa 5 | from python_socks._protocols.http import BasicAuth # noqa 6 | 7 | 8 | @pytest.mark.parametrize('address', ('::1', b'::1', '127.0.0.1', b'127.0.0.1')) 9 | def test_is_ip_address(address): 10 | assert is_ip_address(address) 11 | 12 | 13 | def test_basic_auth(): 14 | login = 'login' 15 | password = 'password' 16 | 17 | auth1 = BasicAuth(login=login, password=password) 18 | auth2 = BasicAuth.decode(auth1.encode()) 19 | 20 | assert auth2.login == login 21 | assert auth2.password == password 22 | -------------------------------------------------------------------------------- /tests/http_app.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import flask # noqa 3 | from flask import request # noqa 4 | 5 | app = flask.Flask(__name__) 6 | 7 | 8 | @app.route('/ip') 9 | def ip(): 10 | return request.remote_addr 11 | 12 | 13 | def run_app(host: str, port: int, certfile: str = None, keyfile: str = None): 14 | if certfile and keyfile: 15 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) 16 | ssl_context.load_cert_chain(certfile, keyfile) 17 | else: 18 | ssl_context = None 19 | 20 | print('Starting http server on {}:{}...'.format(host, port)) 21 | app.run(debug=False, host=host, port=port, threaded=True, 22 | ssl_context=ssl_context) 23 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | 4 | 5 | def is_connectable(host, port): 6 | try: 7 | sock = socket.create_connection((host, port), 1) 8 | except socket.error: 9 | return False 10 | else: 11 | sock.close() 12 | return True 13 | 14 | 15 | def wait_until_connectable(host, port, timeout=10): 16 | count = 0 17 | while not is_connectable(host=host, port=port): 18 | if count >= timeout: 19 | raise Exception( 20 | f'The proxy server has not available by ({host}, {port}) in {timeout:d} seconds' 21 | ) 22 | count += 1 23 | time.sleep(1) 24 | return True 25 | -------------------------------------------------------------------------------- /python_socks/async_/trio/_resolver.py: -------------------------------------------------------------------------------- 1 | import trio 2 | 3 | from ... import _abc as abc 4 | 5 | 6 | class Resolver(abc.AsyncResolver): 7 | async def resolve(self, host, port=0, family=trio.socket.AF_UNSPEC): 8 | infos = await trio.socket.getaddrinfo( 9 | host=host, 10 | port=port, 11 | family=family, 12 | type=trio.socket.SOCK_STREAM, 13 | ) 14 | 15 | if not infos: # pragma: no cover 16 | raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family)) 17 | 18 | infos = sorted(infos, key=lambda info: info[0]) 19 | 20 | family, _, _, _, address = infos[0] 21 | return family, address[0] 22 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/_resolver.py: -------------------------------------------------------------------------------- 1 | import anyio 2 | import socket 3 | 4 | from ... import _abc as abc 5 | 6 | 7 | class Resolver(abc.AsyncResolver): 8 | async def resolve(self, host, port=0, family=socket.AF_UNSPEC): 9 | infos = await anyio.getaddrinfo( 10 | host=host, 11 | port=port, 12 | family=family, 13 | type=socket.SOCK_STREAM, 14 | ) 15 | 16 | if not infos: # pragma: no cover 17 | raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family)) 18 | 19 | infos = sorted(infos, key=lambda info: info[0]) 20 | 21 | family, _, _, _, address = infos[0] 22 | return family, address[0] 23 | -------------------------------------------------------------------------------- /python_socks/sync/v2/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from ._proxy import SyncProxy 3 | 4 | 5 | class ProxyChain: 6 | def __init__(self, proxies: Iterable[SyncProxy]): 7 | self._proxies = proxies 8 | 9 | def connect( 10 | self, 11 | dest_host, 12 | dest_port, 13 | dest_ssl=None, 14 | timeout=None, 15 | ): 16 | forward = None 17 | for proxy in self._proxies: 18 | proxy._forward = forward 19 | forward = proxy 20 | 21 | return forward.connect( 22 | dest_host=dest_host, 23 | dest_port=dest_port, 24 | dest_ssl=dest_ssl, 25 | timeout=timeout, 26 | ) 27 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/v2/_connect.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Optional, Tuple 3 | from ._stream import AsyncioSocketStream 4 | 5 | 6 | async def connect_tcp( 7 | host: str, 8 | port: int, 9 | loop: asyncio.AbstractEventLoop, 10 | local_addr: Optional[Tuple[str, int]] = None, 11 | ) -> AsyncioSocketStream: 12 | kwargs = {} 13 | if local_addr is not None: 14 | kwargs['local_addr'] = local_addr # pragma: no cover 15 | 16 | reader, writer = await asyncio.open_connection( 17 | host=host, 18 | port=port, 19 | **kwargs, # type: ignore 20 | ) 21 | 22 | return AsyncioSocketStream( 23 | loop=loop, 24 | reader=reader, 25 | writer=writer, 26 | ) 27 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/_resolver.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | 4 | from ... import _abc as abc 5 | 6 | 7 | class Resolver(abc.AsyncResolver): 8 | def __init__(self, loop: asyncio.AbstractEventLoop): 9 | self._loop = loop 10 | 11 | async def resolve(self, host, port=0, family=socket.AF_UNSPEC): 12 | infos = await self._loop.getaddrinfo( 13 | host=host, 14 | port=port, 15 | family=family, 16 | type=socket.SOCK_STREAM, 17 | ) 18 | 19 | if not infos: # pragma: no cover 20 | raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family)) 21 | 22 | infos = sorted(infos, key=lambda info: info[0]) 23 | 24 | family, _, _, _, address = infos[0] 25 | return family, address[0] 26 | -------------------------------------------------------------------------------- /python_socks/async_/curio/_resolver.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from curio.socket import getaddrinfo 3 | 4 | from ... import _abc as abc 5 | 6 | 7 | class Resolver(abc.AsyncResolver): 8 | async def resolve(self, host, port=0, family=socket.AF_UNSPEC): 9 | try: 10 | infos = await getaddrinfo( 11 | host=host, 12 | port=port, 13 | family=family, 14 | type=socket.SOCK_STREAM, 15 | ) 16 | except socket.gaierror: # pragma: no cover 17 | infos = None 18 | 19 | if not infos: # pragma: no cover 20 | raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family)) 21 | 22 | infos = sorted(infos, key=lambda info: info[0]) 23 | 24 | family, _, _, _, address = infos[0] 25 | return family, address[0] 26 | -------------------------------------------------------------------------------- /python_socks/async_/trio/v2/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | import warnings 3 | from ._proxy import TrioProxy 4 | 5 | 6 | class ProxyChain: 7 | def __init__(self, proxies: Sequence[TrioProxy]): 8 | warnings.warn( 9 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 10 | DeprecationWarning, 11 | stacklevel=2, 12 | ) 13 | self._proxies = proxies 14 | 15 | async def connect( 16 | self, 17 | dest_host, 18 | dest_port, 19 | dest_ssl=None, 20 | timeout=None, 21 | ): 22 | forward = None 23 | for proxy in self._proxies: 24 | proxy._forward = forward 25 | forward = proxy 26 | 27 | return await forward.connect( 28 | dest_host=dest_host, 29 | dest_port=dest_port, 30 | dest_ssl=dest_ssl, 31 | timeout=timeout, 32 | ) 33 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/v2/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | import warnings 3 | from ._proxy import AnyioProxy 4 | 5 | 6 | class ProxyChain: 7 | def __init__(self, proxies: Sequence[AnyioProxy]): 8 | warnings.warn( 9 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 10 | DeprecationWarning, 11 | stacklevel=2, 12 | ) 13 | self._proxies = proxies 14 | 15 | async def connect( 16 | self, 17 | dest_host, 18 | dest_port, 19 | dest_ssl=None, 20 | timeout=None, 21 | ): 22 | forward = None 23 | for proxy in self._proxies: 24 | proxy._forward = forward 25 | forward = proxy 26 | 27 | return await forward.connect( 28 | dest_host=dest_host, 29 | dest_port=dest_port, 30 | dest_ssl=dest_ssl, 31 | timeout=timeout, 32 | ) 33 | -------------------------------------------------------------------------------- /python_socks/sync/_stream.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from .._errors import ProxyError 4 | from .. import _abc as abc 5 | 6 | DEFAULT_RECEIVE_SIZE = 65536 7 | 8 | 9 | class SyncSocketStream(abc.SyncSocketStream): 10 | _socket: socket.socket 11 | 12 | def __init__(self, sock: socket.socket): 13 | self._socket = sock 14 | 15 | def write_all(self, data): 16 | self._socket.sendall(data) 17 | 18 | def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 19 | return self._socket.recv(max_bytes) 20 | 21 | def read_exact(self, n): 22 | data = bytearray() 23 | while len(data) < n: 24 | packet = self._socket.recv(n - len(data)) 25 | if not packet: # pragma: no cover 26 | raise ProxyError('Connection closed unexpectedly') 27 | data += packet 28 | return data 29 | 30 | def close(self): 31 | if self._socket is not None: 32 | self._socket.close() 33 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/v2/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | import warnings 3 | from ._proxy import AsyncioProxy 4 | 5 | 6 | class ProxyChain: 7 | def __init__(self, proxies: Sequence[AsyncioProxy]): 8 | warnings.warn( 9 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 10 | DeprecationWarning, 11 | stacklevel=2, 12 | ) 13 | self._proxies = proxies 14 | 15 | async def connect( 16 | self, 17 | dest_host: str, 18 | dest_port: int, 19 | dest_ssl=None, 20 | timeout=None, 21 | ): 22 | forward = None 23 | for proxy in self._proxies: 24 | proxy._forward = forward 25 | forward = proxy 26 | 27 | return await forward.connect( 28 | dest_host=dest_host, 29 | dest_port=dest_port, 30 | dest_ssl=dest_ssl, 31 | timeout=timeout, 32 | ) 33 | -------------------------------------------------------------------------------- /python_socks/async_/curio/_stream.py: -------------------------------------------------------------------------------- 1 | import curio.io 2 | import curio.socket 3 | 4 | from ... import _abc as abc 5 | from ..._errors import ProxyError 6 | 7 | DEFAULT_RECEIVE_SIZE = 65536 8 | 9 | 10 | class CurioSocketStream(abc.AsyncSocketStream): 11 | _socket: curio.io.Socket = None 12 | 13 | def __init__(self, sock: curio.io.Socket): 14 | self._socket = sock 15 | 16 | async def write_all(self, data): 17 | await self._socket.sendall(data) 18 | 19 | async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 20 | return await self._socket.recv(max_bytes) 21 | 22 | async def read_exact(self, n): 23 | data = bytearray() 24 | while len(data) < n: 25 | packet = await self._socket.recv(n - len(data)) 26 | if not packet: # pragma: no cover 27 | raise ProxyError('Connection closed unexpectedly') 28 | data += packet 29 | return data 30 | 31 | async def close(self): 32 | await self._socket.close() 33 | -------------------------------------------------------------------------------- /python_socks/async_/trio/_connect.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | 3 | import trio 4 | 5 | from ._resolver import Resolver 6 | from ..._helpers import is_ipv4_address, is_ipv6_address 7 | 8 | 9 | async def connect_tcp( 10 | host: str, 11 | port: int, 12 | local_addr: Optional[Tuple[str, int]] = None, 13 | ) -> trio.socket.SocketType: 14 | 15 | family, host = await _resolve_host(host) 16 | 17 | sock = trio.socket.socket(family=family, type=trio.socket.SOCK_STREAM) 18 | if local_addr is not None: # pragma: no cover 19 | await sock.bind(local_addr) 20 | 21 | try: 22 | await sock.connect((host, port)) 23 | except OSError: 24 | sock.close() 25 | raise 26 | return sock 27 | 28 | 29 | async def _resolve_host(host): 30 | if is_ipv4_address(host): 31 | return trio.socket.AF_INET, host 32 | if is_ipv6_address(host): 33 | return trio.socket.AF_INET6, host 34 | 35 | resolver = Resolver() 36 | return await resolver.resolve(host=host) 37 | -------------------------------------------------------------------------------- /python_socks/_abc.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | class SyncResolver: 5 | def resolve(self, host, port=0, family=0): 6 | raise NotImplementedError() 7 | 8 | 9 | class AsyncResolver: 10 | async def resolve(self, host, port=0, family=0): 11 | raise NotImplementedError() 12 | 13 | 14 | class SyncSocketStream: 15 | 16 | def write_all(self, data: bytes): 17 | raise NotImplementedError() 18 | 19 | def read(self, max_bytes: Optional[int] = None): 20 | raise NotImplementedError() 21 | 22 | def read_exact(self, n: int): 23 | raise NotImplementedError() 24 | 25 | def close(self): 26 | raise NotImplementedError() 27 | 28 | 29 | class AsyncSocketStream: 30 | async def write_all(self, data: bytes): 31 | raise NotImplementedError() 32 | 33 | async def read(self, max_bytes: Optional[int] = None): 34 | raise NotImplementedError() 35 | 36 | async def read_exact(self, n: int): 37 | raise NotImplementedError() 38 | 39 | async def close(self): 40 | raise NotImplementedError() 41 | -------------------------------------------------------------------------------- /python_socks/_connectors/http_sync.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .._abc import SyncSocketStream, SyncResolver 3 | from .abc import SyncConnector 4 | 5 | from .._protocols import http 6 | 7 | 8 | class HttpSyncConnector(SyncConnector): 9 | def __init__( 10 | self, 11 | username: Optional[str], 12 | password: Optional[str], 13 | resolver: SyncResolver, 14 | ): 15 | self._username = username 16 | self._password = password 17 | self._resolver = resolver 18 | 19 | def connect( 20 | self, 21 | stream: SyncSocketStream, 22 | host: str, 23 | port: int, 24 | ) -> http.ConnectReply: 25 | conn = http.Connection() 26 | 27 | request = http.ConnectRequest( 28 | host=host, 29 | port=port, 30 | username=self._username, 31 | password=self._password, 32 | ) 33 | data = conn.send(request) 34 | stream.write_all(data) 35 | 36 | data = stream.read() 37 | reply: http.ConnectReply = conn.receive(data) 38 | return reply 39 | -------------------------------------------------------------------------------- /python_socks/sync/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | import warnings 3 | from ._proxy import SyncProxy 4 | 5 | 6 | class ProxyChain: 7 | def __init__(self, proxies: Iterable[SyncProxy]): 8 | warnings.warn( 9 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 10 | DeprecationWarning, 11 | stacklevel=2, 12 | ) 13 | self._proxies = proxies 14 | 15 | def connect(self, dest_host, dest_port, timeout=None): 16 | curr_socket = None 17 | proxies = list(self._proxies) 18 | 19 | length = len(proxies) - 1 20 | for i in range(length): 21 | curr_socket = proxies[i].connect( 22 | dest_host=proxies[i + 1].proxy_host, 23 | dest_port=proxies[i + 1].proxy_port, 24 | timeout=timeout, 25 | _socket=curr_socket, 26 | ) 27 | 28 | curr_socket = proxies[length].connect( 29 | dest_host=dest_host, dest_port=dest_port, timeout=timeout, _socket=curr_socket 30 | ) 31 | 32 | return curr_socket 33 | -------------------------------------------------------------------------------- /python_socks/_connectors/http_async.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .._abc import AsyncSocketStream, AsyncResolver 3 | from .abc import AsyncConnector 4 | 5 | from .._protocols import http 6 | 7 | 8 | class HttpAsyncConnector(AsyncConnector): 9 | def __init__( 10 | self, 11 | username: Optional[str], 12 | password: Optional[str], 13 | resolver: AsyncResolver, 14 | ): 15 | self._username = username 16 | self._password = password 17 | self._resolver = resolver 18 | 19 | async def connect( 20 | self, 21 | stream: AsyncSocketStream, 22 | host: str, 23 | port: int, 24 | ) -> http.ConnectReply: 25 | conn = http.Connection() 26 | 27 | request = http.ConnectRequest( 28 | host=host, 29 | port=port, 30 | username=self._username, 31 | password=self._password, 32 | ) 33 | data = conn.send(request) 34 | await stream.write_all(data) 35 | 36 | data = await stream.read() 37 | reply: http.ConnectReply = conn.receive(data) 38 | return reply 39 | -------------------------------------------------------------------------------- /python_socks/async_/_proxy_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | import warnings 3 | 4 | 5 | class ProxyChain: 6 | def __init__(self, proxies: Iterable): 7 | warnings.warn( 8 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 9 | DeprecationWarning, 10 | stacklevel=2, 11 | ) 12 | self._proxies = proxies 13 | 14 | async def connect(self, dest_host, dest_port, timeout=None): 15 | curr_socket = None 16 | proxies = list(self._proxies) 17 | 18 | length = len(proxies) - 1 19 | for i in range(length): 20 | curr_socket = await proxies[i].connect( 21 | dest_host=proxies[i + 1].proxy_host, 22 | dest_port=proxies[i + 1].proxy_port, 23 | timeout=timeout, 24 | _socket=curr_socket, 25 | ) 26 | 27 | curr_socket = await proxies[length].connect( 28 | dest_host=dest_host, 29 | dest_port=dest_port, 30 | timeout=timeout, 31 | _socket=curr_socket, 32 | ) 33 | 34 | return curr_socket 35 | -------------------------------------------------------------------------------- /python_socks/async_/trio/_stream.py: -------------------------------------------------------------------------------- 1 | import trio 2 | 3 | from ..._errors import ProxyError 4 | from ... import _abc as abc 5 | 6 | DEFAULT_RECEIVE_SIZE = 65536 7 | 8 | 9 | class TrioSocketStream(abc.AsyncSocketStream): 10 | def __init__(self, sock): 11 | self._socket = sock 12 | 13 | async def write_all(self, data): 14 | total_sent = 0 15 | while total_sent < len(data): 16 | remaining = data[total_sent:] 17 | sent = await self._socket.send(remaining) 18 | total_sent += sent 19 | 20 | async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 21 | return await self._socket.recv(max_bytes) 22 | 23 | async def read_exact(self, n): 24 | data = bytearray() 25 | while len(data) < n: 26 | packet = await self._socket.recv(n - len(data)) 27 | if not packet: # pragma: no cover 28 | raise ProxyError('Connection closed unexpectedly') 29 | data += packet 30 | return data 31 | 32 | async def close(self): 33 | if self._socket is not None: 34 | self._socket.close() 35 | await trio.lowlevel.checkpoint() 36 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/_stream.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | 4 | from ..._errors import ProxyError 5 | 6 | from ... import _abc as abc 7 | 8 | DEFAULT_RECEIVE_SIZE = 65536 9 | 10 | 11 | class AsyncioSocketStream(abc.AsyncSocketStream): 12 | _loop: asyncio.AbstractEventLoop = None 13 | _socket = None 14 | 15 | def __init__(self, sock: socket.socket, loop: asyncio.AbstractEventLoop): 16 | self._loop = loop 17 | self._socket = sock 18 | 19 | async def write_all(self, data): 20 | await self._loop.sock_sendall(self._socket, data) 21 | 22 | async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 23 | return await self._loop.sock_recv(self._socket, max_bytes) 24 | 25 | async def read_exact(self, n): 26 | data = bytearray() 27 | while len(data) < n: 28 | packet = await self._loop.sock_recv(self._socket, n - len(data)) 29 | if not packet: # pragma: no cover 30 | raise ProxyError('Connection closed unexpectedly') 31 | data += packet 32 | return data 33 | 34 | async def close(self): 35 | if self._socket is not None: 36 | self._socket.close() 37 | -------------------------------------------------------------------------------- /python_socks/_connectors/factory_sync.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .._abc import SyncResolver 3 | from .._types import ProxyType 4 | 5 | from .abc import SyncConnector 6 | from .socks5_sync import Socks5SyncConnector 7 | from .socks4_sync import Socks4SyncConnector 8 | from .http_sync import HttpSyncConnector 9 | 10 | 11 | def create_connector( 12 | proxy_type: ProxyType, 13 | username: Optional[str], 14 | password: Optional[str], 15 | rdns: Optional[bool], 16 | resolver: SyncResolver, 17 | ) -> SyncConnector: 18 | if proxy_type == ProxyType.SOCKS4: 19 | return Socks4SyncConnector( 20 | user_id=username, 21 | rdns=rdns, 22 | resolver=resolver, 23 | ) 24 | 25 | if proxy_type == ProxyType.SOCKS5: 26 | return Socks5SyncConnector( 27 | username=username, 28 | password=password, 29 | rdns=rdns, 30 | resolver=resolver, 31 | ) 32 | 33 | if proxy_type == ProxyType.HTTP: 34 | return HttpSyncConnector( 35 | username=username, 36 | password=password, 37 | resolver=resolver, 38 | ) 39 | 40 | raise ValueError(f'Invalid proxy type: {proxy_type}') 41 | -------------------------------------------------------------------------------- /python_socks/_connectors/factory_async.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from .._abc import AsyncResolver 3 | from .._types import ProxyType 4 | 5 | from .abc import AsyncConnector 6 | from .socks5_async import Socks5AsyncConnector 7 | from .socks4_async import Socks4AsyncConnector 8 | from .http_async import HttpAsyncConnector 9 | 10 | 11 | def create_connector( 12 | proxy_type: ProxyType, 13 | username: Optional[str], 14 | password: Optional[str], 15 | rdns: Optional[bool], 16 | resolver: AsyncResolver, 17 | ) -> AsyncConnector: 18 | if proxy_type == ProxyType.SOCKS4: 19 | return Socks4AsyncConnector( 20 | user_id=username, 21 | rdns=rdns, 22 | resolver=resolver, 23 | ) 24 | 25 | if proxy_type == ProxyType.SOCKS5: 26 | return Socks5AsyncConnector( 27 | username=username, 28 | password=password, 29 | rdns=rdns, 30 | resolver=resolver, 31 | ) 32 | 33 | if proxy_type == ProxyType.HTTP: 34 | return HttpAsyncConnector( 35 | username=username, 36 | password=password, 37 | resolver=resolver, 38 | ) 39 | 40 | raise ValueError(f'Invalid proxy type: {proxy_type}') 41 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/_chain.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | import warnings 3 | from ._proxy import AnyioProxy 4 | 5 | 6 | class ProxyChain: 7 | def __init__(self, proxies: Iterable[AnyioProxy]): 8 | warnings.warn( 9 | 'This implementation of ProxyChain is deprecated and will be removed in the future', 10 | DeprecationWarning, 11 | stacklevel=2, 12 | ) 13 | self._proxies = proxies 14 | 15 | async def connect( 16 | self, 17 | dest_host, 18 | dest_port, 19 | dest_ssl=None, 20 | timeout=None, 21 | ): 22 | _stream = None 23 | proxies = list(self._proxies) 24 | 25 | length = len(proxies) - 1 26 | for i in range(length): 27 | _stream = await proxies[i].connect( 28 | dest_host=proxies[i + 1].proxy_host, 29 | dest_port=proxies[i + 1].proxy_port, 30 | timeout=timeout, 31 | _stream=_stream, 32 | ) 33 | 34 | _stream = await proxies[length].connect( 35 | dest_host=dest_host, 36 | dest_port=dest_port, 37 | dest_ssl=dest_ssl, 38 | timeout=timeout, 39 | _stream=_stream, 40 | ) 41 | 42 | return _stream 43 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/_connect.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import asyncio 3 | from typing import Optional, Tuple 4 | 5 | from ._resolver import Resolver 6 | from ..._helpers import is_ipv4_address, is_ipv6_address 7 | 8 | 9 | async def connect_tcp( 10 | host: str, 11 | port: int, 12 | loop: asyncio.AbstractEventLoop, 13 | local_addr: Optional[Tuple[str, int]] = None, 14 | ) -> socket.socket: 15 | 16 | family, host = await _resolve_host(host, loop) 17 | 18 | sock = socket.socket(family=family, type=socket.SOCK_STREAM) 19 | sock.setblocking(False) 20 | if local_addr is not None: # pragma: no cover 21 | sock.bind(local_addr) 22 | 23 | if is_ipv6_address(host): 24 | address = (host, port, 0, 0) # to fix OSError: [WinError 10022] 25 | else: 26 | address = (host, port) # type: ignore[assignment] 27 | 28 | try: 29 | await loop.sock_connect(sock=sock, address=address) 30 | except OSError: 31 | sock.close() 32 | raise 33 | return sock 34 | 35 | 36 | async def _resolve_host(host, loop): 37 | if is_ipv4_address(host): 38 | return socket.AF_INET, host 39 | if is_ipv6_address(host): 40 | return socket.AF_INET6, host 41 | 42 | resolver = Resolver(loop=loop) 43 | return await resolver.resolve(host=host) 44 | -------------------------------------------------------------------------------- /python_socks/_connectors/socks4_sync.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional 3 | 4 | from .._abc import SyncSocketStream, SyncResolver 5 | from .abc import SyncConnector 6 | 7 | from .._protocols import socks4 8 | from .._helpers import is_ip_address 9 | 10 | 11 | class Socks4SyncConnector(SyncConnector): 12 | def __init__( 13 | self, 14 | user_id: Optional[str], 15 | rdns: Optional[bool], 16 | resolver: SyncResolver, 17 | ): 18 | if rdns is None: 19 | rdns = False 20 | 21 | self._user_id = user_id 22 | self._rdns = rdns 23 | self._resolver = resolver 24 | 25 | def connect( 26 | self, 27 | stream: SyncSocketStream, 28 | host: str, 29 | port: int, 30 | ) -> socks4.ConnectReply: 31 | conn = socks4.Connection() 32 | 33 | if not is_ip_address(host) and not self._rdns: 34 | _, host = self._resolver.resolve( 35 | host, 36 | family=socket.AF_INET, 37 | ) 38 | 39 | request = socks4.ConnectRequest(host=host, port=port, user_id=self._user_id) 40 | data = conn.send(request) 41 | stream.write_all(data) 42 | 43 | data = stream.read_exact(socks4.ConnectReply.SIZE) 44 | reply: socks4.ConnectReply = conn.receive(data) 45 | return reply 46 | -------------------------------------------------------------------------------- /python_socks/_connectors/socks4_async.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional 3 | 4 | from .._abc import AsyncSocketStream, AsyncResolver 5 | from .abc import AsyncConnector 6 | 7 | from .._protocols import socks4 8 | from .._helpers import is_ip_address 9 | 10 | 11 | class Socks4AsyncConnector(AsyncConnector): 12 | def __init__( 13 | self, 14 | user_id: Optional[str], 15 | rdns: Optional[bool], 16 | resolver: AsyncResolver, 17 | ): 18 | if rdns is None: 19 | rdns = False 20 | 21 | self._user_id = user_id 22 | self._rdns = rdns 23 | self._resolver = resolver 24 | 25 | async def connect( 26 | self, 27 | stream: AsyncSocketStream, 28 | host: str, 29 | port: int, 30 | ) -> socks4.ConnectReply: 31 | conn = socks4.Connection() 32 | 33 | if not is_ip_address(host) and not self._rdns: 34 | _, host = await self._resolver.resolve( 35 | host, 36 | family=socket.AF_INET, 37 | ) 38 | 39 | request = socks4.ConnectRequest(host=host, port=port, user_id=self._user_id) 40 | data = conn.send(request) 41 | await stream.write_all(data) 42 | 43 | data = await stream.read_exact(socks4.ConnectReply.SIZE) 44 | reply: socks4.ConnectReply = conn.receive(data) 45 | return reply 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | build: 11 | name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" 12 | runs-on: "${{ matrix.os }}" 13 | strategy: 14 | matrix: 15 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 16 | os: [ubuntu-latest] 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Setup Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip setuptools 27 | pip install -r requirements-dev.txt 28 | - name: Lint with flake8 29 | run: | 30 | python -m flake8 python_socks tests 31 | continue-on-error: true 32 | - name: Run tests 33 | # run: python -m pytest tests --cov=./python_socks --cov-report term-missing -s 34 | run: python -m pytest tests --cov=./python_socks --cov-report xml 35 | - name: Upload coverage 36 | uses: codecov/codecov-action@v5 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | slug: romis2012/python-socks 40 | file: ./coverage.xml 41 | flags: unit 42 | fail_ci_if_error: false -------------------------------------------------------------------------------- /tests/http_server.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import time 3 | from multiprocessing import Process 4 | 5 | from tests.utils import is_connectable 6 | from tests.http_app import run_app 7 | 8 | 9 | class HttpServerConfig(typing.NamedTuple): 10 | host: str 11 | port: int 12 | certfile: str = None 13 | keyfile: str = None 14 | 15 | def to_dict(self): 16 | d = {} 17 | for key, val in self._asdict().items(): 18 | if val is not None: 19 | d[key] = val 20 | return d 21 | 22 | 23 | class HttpServer: 24 | def __init__(self, config: typing.Iterable[HttpServerConfig]): 25 | self.config = config 26 | self.workers = [] 27 | 28 | def start(self): 29 | for cfg in self.config: 30 | p = Process(target=run_app, kwargs=cfg.to_dict()) 31 | self.workers.append(p) 32 | 33 | for p in self.workers: 34 | p.start() 35 | 36 | def terminate(self): 37 | for p in self.workers: 38 | p.terminate() 39 | 40 | def wait_until_connectable(self, host, port, timeout=10): 41 | count = 0 42 | while not is_connectable(host=host, port=port): 43 | if count >= timeout: 44 | self.terminate() 45 | raise Exception( 46 | 'The http server has not available ' 47 | 'by (%s, %s) in %d seconds' 48 | % (host, port, timeout)) 49 | count += 1 50 | time.sleep(1) 51 | return True 52 | -------------------------------------------------------------------------------- /python_socks/async_/trio/v2/_stream.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Union 3 | 4 | import trio 5 | 6 | from ...._errors import ProxyError 7 | from .... import _abc as abc 8 | 9 | DEFAULT_RECEIVE_SIZE = 65536 10 | 11 | TrioStreamType = Union[trio.SocketStream, trio.SSLStream] 12 | 13 | 14 | class TrioSocketStream(abc.AsyncSocketStream): 15 | _stream: TrioStreamType 16 | 17 | def __init__(self, stream: TrioStreamType): 18 | self._stream = stream 19 | 20 | async def write_all(self, data): 21 | await self._stream.send_all(data) 22 | 23 | async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 24 | return await self._stream.receive_some(max_bytes) 25 | 26 | async def read_exact(self, n): 27 | data = bytearray() 28 | while len(data) < n: 29 | packet = await self._stream.receive_some(n - len(data)) 30 | if not packet: # pragma: no cover 31 | raise ProxyError('Connection closed unexpectedly') 32 | data += packet 33 | return data 34 | 35 | async def start_tls( 36 | self, 37 | hostname: str, 38 | ssl_context: ssl.SSLContext, 39 | ) -> 'TrioSocketStream': 40 | ssl_stream = trio.SSLStream( 41 | self._stream, 42 | ssl_context=ssl_context, 43 | server_hostname=hostname, 44 | https_compatible=True, 45 | server_side=False, 46 | ) 47 | await ssl_stream.do_handshake() 48 | return TrioSocketStream(ssl_stream) 49 | 50 | async def close(self): 51 | await self._stream.aclose() 52 | 53 | @property 54 | def trio_stream(self) -> TrioStreamType: # pragma: nocover 55 | return self._stream 56 | -------------------------------------------------------------------------------- /python_socks/sync/v2/_stream.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import ssl 3 | from typing import Union 4 | 5 | from ._ssl_transport import SSLTransport 6 | 7 | from ..._errors import ProxyError 8 | from ... import _abc as abc 9 | 10 | DEFAULT_RECEIVE_SIZE = 65536 11 | 12 | SocketType = Union[socket.socket, ssl.SSLSocket, SSLTransport] 13 | 14 | 15 | class SyncSocketStream(abc.SyncSocketStream): 16 | _socket: SocketType 17 | 18 | def __init__(self, sock: SocketType): 19 | self._socket = sock 20 | 21 | def write_all(self, data): 22 | self._socket.sendall(data) 23 | 24 | def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 25 | return self._socket.recv(max_bytes) 26 | 27 | def read_exact(self, n): 28 | data = bytearray() 29 | while len(data) < n: 30 | packet = self._socket.recv(n - len(data)) 31 | if not packet: # pragma: no cover 32 | raise ProxyError('Connection closed unexpectedly') 33 | data += packet 34 | return data 35 | 36 | def start_tls(self, hostname: str, ssl_context: ssl.SSLContext) -> 'SyncSocketStream': 37 | if isinstance(self._socket, (ssl.SSLSocket, SSLTransport)): 38 | ssl_socket = SSLTransport( 39 | self._socket, 40 | ssl_context=ssl_context, 41 | server_hostname=hostname, 42 | ) 43 | else: # plain socket? 44 | ssl_socket = ssl_context.wrap_socket( 45 | self._socket, 46 | server_hostname=hostname, 47 | ) 48 | 49 | return SyncSocketStream(ssl_socket) 50 | 51 | def close(self): 52 | self._socket.close() 53 | 54 | @property 55 | def socket(self) -> SocketType: # pragma: nocover 56 | return self._socket 57 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/_stream.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Union 3 | 4 | import anyio 5 | import anyio.abc 6 | from anyio.streams.tls import TLSStream 7 | 8 | from ..._errors import ProxyError 9 | from ... import _abc as abc 10 | 11 | DEFAULT_RECEIVE_SIZE = 65536 12 | 13 | AnyioStreamType = Union[anyio.abc.SocketStream, TLSStream] 14 | 15 | 16 | class AnyioSocketStream(abc.AsyncSocketStream): 17 | _stream: AnyioStreamType 18 | 19 | def __init__(self, stream: AnyioStreamType) -> None: 20 | self._stream = stream 21 | 22 | async def write_all(self, data: bytes): 23 | await self._stream.send(item=data) 24 | 25 | async def read(self, max_bytes: int = DEFAULT_RECEIVE_SIZE): 26 | try: 27 | return await self._stream.receive(max_bytes=max_bytes) 28 | except anyio.EndOfStream: # pragma: no cover 29 | return b"" 30 | 31 | async def read_exact(self, n: int): 32 | data = bytearray() 33 | while len(data) < n: 34 | packet = await self.read(n - len(data)) 35 | if not packet: # pragma: no cover 36 | raise ProxyError('Connection closed unexpectedly') 37 | data += packet 38 | return data 39 | 40 | async def start_tls( 41 | self, 42 | hostname: str, 43 | ssl_context: ssl.SSLContext, 44 | ) -> 'AnyioSocketStream': 45 | ssl_stream = await TLSStream.wrap( 46 | self._stream, 47 | ssl_context=ssl_context, 48 | hostname=hostname, 49 | standard_compatible=False, 50 | server_side=False, 51 | ) 52 | return AnyioSocketStream(ssl_stream) 53 | 54 | async def close(self): 55 | await self._stream.aclose() 56 | 57 | @property 58 | def anyio_stream(self) -> AnyioStreamType: # pragma: no cover 59 | return self._stream 60 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/v2/_stream.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Union 3 | 4 | import anyio 5 | import anyio.abc 6 | from anyio.streams.tls import TLSStream 7 | 8 | from ...._errors import ProxyError 9 | from .... import _abc as abc 10 | 11 | DEFAULT_RECEIVE_SIZE = 65536 12 | 13 | AnyioStreamType = Union[anyio.abc.SocketStream, TLSStream] 14 | 15 | 16 | class AnyioSocketStream(abc.AsyncSocketStream): 17 | _stream: AnyioStreamType 18 | 19 | def __init__(self, stream: AnyioStreamType) -> None: 20 | self._stream = stream 21 | 22 | async def write_all(self, data: bytes): 23 | await self._stream.send(item=data) 24 | 25 | async def read(self, max_bytes: int = DEFAULT_RECEIVE_SIZE): 26 | try: 27 | return await self._stream.receive(max_bytes=max_bytes) 28 | except anyio.EndOfStream: # pragma: no cover 29 | return b"" 30 | 31 | async def read_exact(self, n: int): 32 | data = bytearray() 33 | while len(data) < n: 34 | packet = await self.read(n - len(data)) 35 | if not packet: # pragma: no cover 36 | raise ProxyError('Connection closed unexpectedly') 37 | data += packet 38 | return data 39 | 40 | async def start_tls( 41 | self, 42 | hostname: str, 43 | ssl_context: ssl.SSLContext, 44 | ) -> 'AnyioSocketStream': 45 | ssl_stream = await TLSStream.wrap( 46 | self._stream, 47 | ssl_context=ssl_context, 48 | hostname=hostname, 49 | standard_compatible=False, 50 | server_side=False, 51 | ) 52 | return AnyioSocketStream(ssl_stream) 53 | 54 | async def close(self): 55 | await self._stream.aclose() 56 | 57 | @property 58 | def anyio_stream(self) -> AnyioStreamType: # pragma: no cover 59 | return self._stream 60 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools'] 3 | build-backend = 'setuptools.build_meta' 4 | 5 | [project] 6 | name = 'python-socks' 7 | license = { text = 'Apache-2.0' } 8 | description = 'Proxy (SOCKS4, SOCKS5, HTTP CONNECT) client for Python' 9 | readme = 'README.md' 10 | authors = [{ name = 'Roman Snegirev', email = 'snegiryev@gmail.com' }] 11 | keywords = [ 12 | 'socks', 13 | 'socks5', 14 | 'socks4', 15 | 'http', 16 | 'proxy', 17 | 'asyncio', 18 | 'trio', 19 | 'curio', 20 | 'anyio', 21 | ] 22 | requires-python = ">=3.8.0" 23 | dynamic = ['version'] 24 | classifiers = [ 25 | "Development Status :: 5 - Production/Stable", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | "Programming Language :: Python :: 3.14", 36 | "Operating System :: MacOS", 37 | "Operating System :: Microsoft", 38 | "Operating System :: POSIX :: Linux", 39 | "Topic :: Internet :: WWW/HTTP", 40 | "Intended Audience :: Developers", 41 | "Framework :: AsyncIO", 42 | "Framework :: Trio", 43 | "License :: OSI Approved :: Apache Software License", 44 | ] 45 | 46 | [project.optional-dependencies] 47 | asyncio = ['async-timeout>=4.0; python_version < "3.11"'] 48 | trio = ['trio>=0.24'] 49 | curio = ['curio>=1.4'] 50 | anyio = ['anyio>=3.3.4,<5.0.0'] 51 | 52 | [project.urls] 53 | homepage = 'https://github.com/romis2012/python-socks' 54 | repository = 'https://github.com/romis2012/python-socks' 55 | 56 | [tool.setuptools.dynamic] 57 | version = { attr = 'python_socks.__version__' } 58 | 59 | [tool.setuptools.packages.find] 60 | include = ['python_socks*'] 61 | 62 | [tool.black] 63 | line-length = 99 64 | target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] 65 | skip-string-normalization = true 66 | preview = true 67 | verbose = true 68 | 69 | [tool.pytest.ini_options] 70 | asyncio_mode = 'strict' 71 | -------------------------------------------------------------------------------- /tests/mocks.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from tests.config import ( 4 | TEST_HOST_NAME_IPV4, 5 | PROXY_HOST_NAME_IPV4, 6 | TEST_HOST_NAME_IPV6, 7 | PROXY_HOST_NAME_IPV6, 8 | ) 9 | 10 | 11 | def getaddrinfo_sync_mock(): 12 | _orig_getaddrinfo = socket.getaddrinfo 13 | 14 | def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): 15 | if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): 16 | return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.0.0.1', port))] 17 | 18 | if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): 19 | return [(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', port, 0, 0))] 20 | 21 | return _orig_getaddrinfo(host, port, family, type, proto, flags) 22 | 23 | return getaddrinfo 24 | 25 | 26 | def getaddrinfo_async_mock(origin_getaddrinfo): 27 | async def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): 28 | if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): 29 | return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.0.0.1', port))] 30 | 31 | if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): 32 | return [(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', port, 0, 0))] 33 | 34 | return await origin_getaddrinfo( 35 | host, 36 | port, 37 | family=family, 38 | type=type, 39 | proto=proto, 40 | flags=flags, 41 | ) 42 | 43 | return getaddrinfo 44 | 45 | 46 | def _resolve_local(host): 47 | if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): 48 | return socket.AF_INET, '127.0.0.1' 49 | 50 | if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): 51 | return socket.AF_INET6, '::1' 52 | 53 | return None 54 | 55 | 56 | def sync_resolve_factory(cls): 57 | original_resolve = cls.resolve 58 | 59 | def new_resolve(self, host, port=0, family=socket.AF_UNSPEC): 60 | res = _resolve_local(host) 61 | 62 | if res is not None: 63 | return res 64 | 65 | return original_resolve(self, host=host, port=port, family=family) 66 | 67 | return new_resolve 68 | 69 | 70 | def async_resolve_factory(cls): 71 | original_resolve = cls.resolve 72 | 73 | async def new_resolve(self, host, port=0, family=socket.AF_UNSPEC): 74 | res = _resolve_local(host) 75 | 76 | if res is not None: 77 | return res 78 | 79 | return await original_resolve(self, host=host, port=port, family=family) 80 | 81 | return new_resolve 82 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | LOGIN = 'admin' 4 | PASSWORD = 'admin' 5 | 6 | PROXY_HOST_IPV4 = '127.0.0.1' 7 | PROXY_HOST_IPV6 = '::1' 8 | 9 | PROXY_HOST_NAME_IPV4 = 'ip4.proxy.example.com' 10 | PROXY_HOST_NAME_IPV6 = 'ip6.proxy.example.com' 11 | 12 | SOCKS5_PROXY_PORT = 7780 13 | SOCKS5_PROXY_PORT_NO_AUTH = 7781 14 | 15 | SOCKS4_PROXY_PORT = 7782 16 | SOCKS4_PORT_NO_AUTH = 7783 17 | 18 | HTTP_PROXY_PORT = 7784 19 | HTTPS_PROXY_PORT = 7785 20 | 21 | SKIP_IPV6_TESTS = 'SKIP_IPV6_TESTS' in os.environ 22 | 23 | SOCKS5_IPV4_URL = 'socks5://{login}:{password}@{host}:{port}'.format( 24 | host=PROXY_HOST_IPV4, 25 | port=SOCKS5_PROXY_PORT, 26 | login=LOGIN, 27 | password=PASSWORD, 28 | ) 29 | 30 | SOCKS5_IPV6_URL = 'socks5://{login}:{password}@{host}:{port}'.format( 31 | host='[%s]' % PROXY_HOST_IPV6, 32 | port=SOCKS5_PROXY_PORT, 33 | login=LOGIN, 34 | password=PASSWORD, 35 | ) 36 | 37 | SOCKS5_IPV4_HOSTNAME_URL = 'socks5://{login}:{password}@{host}:{port}'.format( 38 | host=PROXY_HOST_NAME_IPV4, 39 | port=SOCKS5_PROXY_PORT, 40 | login=LOGIN, 41 | password=PASSWORD, 42 | ) 43 | 44 | SOCKS5_IPV4_URL_WO_AUTH = 'socks5://{host}:{port}'.format( 45 | host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT_NO_AUTH 46 | ) 47 | 48 | SOCKS4_URL = 'socks4://{login}:{password}@{host}:{port}'.format( 49 | host=PROXY_HOST_IPV4, 50 | port=SOCKS4_PROXY_PORT, 51 | login=LOGIN, 52 | password='', 53 | ) 54 | 55 | HTTP_PROXY_URL = 'http://{login}:{password}@{host}:{port}'.format( 56 | host=PROXY_HOST_IPV4, 57 | port=HTTP_PROXY_PORT, 58 | login=LOGIN, 59 | password=PASSWORD, 60 | ) 61 | 62 | HTTPS_PROXY_URL = 'http://{login}:{password}@{host}:{port}'.format( 63 | host=PROXY_HOST_NAME_IPV4, 64 | port=HTTPS_PROXY_PORT, 65 | login=LOGIN, 66 | password=PASSWORD, 67 | ) 68 | 69 | TEST_HOST_IPV4 = '127.0.0.1' 70 | TEST_HOST_IPV6 = '::1' 71 | 72 | TEST_HOST_NAME_IPV4 = 'ip4.target.example.com' 73 | TEST_HOST_NAME_IPV6 = 'ip6.target.example.com' 74 | 75 | TEST_PORT_IPV4 = 8889 76 | TEST_PORT_IPV6 = 8889 77 | 78 | TEST_PORT_IPV4_HTTPS = 8890 79 | 80 | TEST_URL_IPV4 = 'http://{host}:{port}/ip'.format(host=TEST_HOST_NAME_IPV4, port=TEST_PORT_IPV4) 81 | 82 | TEST_URL_IPv6 = 'http://{host}:{port}/ip'.format(host=TEST_HOST_NAME_IPV6, port=TEST_PORT_IPV6) 83 | 84 | TEST_URL_IPV4_HTTPS = 'https://{host}:{port}/ip'.format( 85 | host=TEST_HOST_NAME_IPV4, port=TEST_PORT_IPV4_HTTPS 86 | ) 87 | 88 | 89 | def resolve_path(path): 90 | return os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), path)) 91 | -------------------------------------------------------------------------------- /python_socks/_connectors/socks5_sync.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional 3 | 4 | from .._abc import SyncSocketStream, SyncResolver 5 | from .abc import SyncConnector 6 | 7 | from .._protocols import socks5 8 | from .._helpers import is_ip_address 9 | 10 | 11 | class Socks5SyncConnector(SyncConnector): 12 | def __init__( 13 | self, 14 | username: Optional[str], 15 | password: Optional[str], 16 | rdns: Optional[bool], 17 | resolver: SyncResolver, 18 | ): 19 | if rdns is None: 20 | rdns = True 21 | 22 | self._username = username 23 | self._password = password 24 | self._rdns = rdns 25 | self._resolver = resolver 26 | 27 | def connect( 28 | self, 29 | stream: SyncSocketStream, 30 | host: str, 31 | port: int, 32 | ) -> socks5.ConnectReply: 33 | conn = socks5.Connection() 34 | 35 | # Auth methods 36 | request = socks5.AuthMethodsRequest(username=self._username, password=self._password) 37 | data = conn.send(request) 38 | stream.write_all(data) 39 | 40 | data = stream.read_exact(socks5.AuthMethodReply.SIZE) 41 | reply: socks5.AuthMethodReply = conn.receive(data) 42 | 43 | # Authenticate 44 | if reply.method == socks5.AuthMethod.USERNAME_PASSWORD: 45 | request = socks5.AuthRequest(username=self._username, password=self._password) 46 | data = conn.send(request) 47 | stream.write_all(data) 48 | 49 | data = stream.read_exact(socks5.AuthReply.SIZE) 50 | _: socks5.AuthReply = conn.receive(data) 51 | 52 | # Connect 53 | if not is_ip_address(host) and not self._rdns: 54 | _, host = self._resolver.resolve(host, family=socket.AF_UNSPEC) 55 | 56 | request = socks5.ConnectRequest(host=host, port=port) 57 | data = conn.send(request) 58 | stream.write_all(data) 59 | 60 | data = self._read_reply(stream) 61 | reply: socks5.ConnectReply = conn.receive(data) 62 | return reply 63 | 64 | # noinspection PyMethodMayBeStatic 65 | def _read_reply(self, stream: SyncSocketStream) -> bytes: 66 | data = stream.read_exact(4) 67 | if data[0] != socks5.SOCKS_VER: 68 | return data 69 | if data[1] != socks5.ReplyCode.SUCCEEDED: 70 | return data 71 | if data[2] != socks5.RSV: 72 | return data 73 | 74 | addr_type = data[3] 75 | 76 | if addr_type == socks5.AddressType.IPV4: 77 | data += stream.read_exact(6) 78 | elif addr_type == socks5.AddressType.IPV6: 79 | data += stream.read_exact(18) 80 | elif addr_type == socks5.AddressType.DOMAIN: 81 | data += stream.read_exact(1) 82 | host_len = data[-1] 83 | data += stream.read_exact(host_len + 2) 84 | 85 | return data 86 | -------------------------------------------------------------------------------- /python_socks/_helpers.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | from typing import Optional, Tuple 4 | from urllib.parse import urlparse, unquote 5 | 6 | from ._types import ProxyType 7 | 8 | # pylint:disable-next=invalid-name 9 | _ipv4_pattern = ( 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 | ) 13 | 14 | # pylint:disable-next=invalid-name 15 | _ipv6_pattern = ( 16 | r'^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}' 17 | r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)' 18 | r'((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})' 19 | r'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}' 20 | r'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}' 21 | r'[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)' 22 | r'(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}' 23 | r':|:(:[A-F0-9]{1,4}){7})$' 24 | ) 25 | 26 | _ipv4_regex = re.compile(_ipv4_pattern) 27 | _ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE) 28 | _ipv4_regexb = re.compile(_ipv4_pattern.encode('ascii')) 29 | _ipv6_regexb = re.compile(_ipv6_pattern.encode('ascii'), flags=re.IGNORECASE) 30 | 31 | 32 | def _is_ip_address(regex, regexb, host): 33 | # if host is None: 34 | # return False 35 | if isinstance(host, str): 36 | return bool(regex.match(host)) 37 | elif isinstance(host, (bytes, bytearray, memoryview)): 38 | return bool(regexb.match(host)) 39 | else: 40 | raise TypeError( 41 | '{} [{}] is not a str or bytes'.format(host, type(host)) # pragma: no cover 42 | ) 43 | 44 | 45 | is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb) 46 | is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb) 47 | 48 | 49 | def is_ip_address(host): 50 | return is_ipv4_address(host) or is_ipv6_address(host) 51 | 52 | 53 | def parse_proxy_url(url: str) -> Tuple[ProxyType, str, int, Optional[str], Optional[str]]: 54 | parsed = urlparse(url) 55 | 56 | scheme = parsed.scheme 57 | if scheme == 'socks5': 58 | proxy_type = ProxyType.SOCKS5 59 | elif scheme == 'socks4': 60 | proxy_type = ProxyType.SOCKS4 61 | elif scheme == 'http': 62 | proxy_type = ProxyType.HTTP 63 | else: 64 | raise ValueError(f'Invalid scheme component: {scheme}') # pragma: no cover 65 | 66 | host = parsed.hostname 67 | if not host: 68 | raise ValueError('Empty host component') # pragma: no cover 69 | 70 | try: 71 | port = parsed.port 72 | assert port is not None 73 | except (ValueError, TypeError, AssertionError) as e: # pragma: no cover 74 | raise ValueError('Invalid port component') from e 75 | 76 | try: 77 | username, password = (unquote(parsed.username), unquote(parsed.password)) 78 | except (AttributeError, TypeError): 79 | username, password = '', '' 80 | 81 | return proxy_type, host, port, username, password 82 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/v2/_stream.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import ssl 3 | 4 | from .... import _abc as abc 5 | 6 | DEFAULT_RECEIVE_SIZE = 65536 7 | 8 | 9 | class AsyncioSocketStream(abc.AsyncSocketStream): 10 | _loop: asyncio.AbstractEventLoop 11 | _reader: asyncio.StreamReader 12 | _writer: asyncio.StreamWriter 13 | 14 | def __init__( 15 | self, 16 | loop: asyncio.AbstractEventLoop, 17 | reader: asyncio.StreamReader, 18 | writer: asyncio.StreamWriter, 19 | ): 20 | self._loop = loop 21 | self._reader = reader 22 | self._writer = writer 23 | 24 | async def write_all(self, data): 25 | self._writer.write(data) 26 | await self._writer.drain() 27 | 28 | async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE): 29 | return await self._reader.read(max_bytes) 30 | 31 | async def read_exact(self, n): 32 | return await self._reader.readexactly(n) 33 | 34 | async def start_tls( 35 | self, 36 | hostname: str, 37 | ssl_context: ssl.SSLContext, 38 | ssl_handshake_timeout=None, 39 | ) -> 'AsyncioSocketStream': 40 | if hasattr(self._writer, 'start_tls'): # Python>=3.11 41 | await self._writer.start_tls( 42 | ssl_context, 43 | server_hostname=hostname, 44 | ssl_handshake_timeout=ssl_handshake_timeout, 45 | ) 46 | return self 47 | 48 | reader = asyncio.StreamReader() 49 | protocol = asyncio.StreamReaderProtocol(reader) 50 | 51 | transport: asyncio.Transport = await self._loop.start_tls( 52 | self._writer.transport, # type: ignore 53 | protocol, 54 | ssl_context, 55 | server_side=False, 56 | server_hostname=hostname, 57 | ssl_handshake_timeout=ssl_handshake_timeout, 58 | ) 59 | 60 | # reader.set_transport(transport) 61 | 62 | # Initialize the protocol, so it is made aware of being tied to 63 | # a TLS connection. 64 | # See: https://github.com/encode/httpx/issues/859 65 | protocol.connection_made(transport) 66 | 67 | writer = asyncio.StreamWriter( 68 | transport=transport, 69 | protocol=protocol, 70 | reader=reader, 71 | loop=self._loop, 72 | ) 73 | 74 | stream = AsyncioSocketStream(loop=self._loop, reader=reader, writer=writer) 75 | # When we return a new SocketStream with new StreamReader/StreamWriter instances 76 | # we need to keep references to the old StreamReader/StreamWriter so that they 77 | # are not garbage collected and closed while we're still using them. 78 | stream._inner = self # type: ignore # pylint:disable=W0212,W0201 79 | return stream 80 | 81 | async def close(self): 82 | self._writer.close() 83 | self._writer.transport.abort() # noqa 84 | 85 | @property 86 | def reader(self): 87 | return self._reader # pragma: no cover 88 | 89 | @property 90 | def writer(self): 91 | return self._writer # pragma: no cover 92 | -------------------------------------------------------------------------------- /python_socks/_connectors/socks5_async.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional 3 | 4 | from .._abc import AsyncSocketStream, AsyncResolver 5 | from .abc import AsyncConnector 6 | 7 | from .._protocols import socks5 8 | from .._helpers import is_ip_address 9 | 10 | 11 | class Socks5AsyncConnector(AsyncConnector): 12 | def __init__( 13 | self, 14 | username: Optional[str], 15 | password: Optional[str], 16 | rdns: Optional[bool], 17 | resolver: AsyncResolver, 18 | ): 19 | if rdns is None: 20 | rdns = True 21 | 22 | self._username = username 23 | self._password = password 24 | self._rdns = rdns 25 | self._resolver = resolver 26 | 27 | async def connect( 28 | self, 29 | stream: AsyncSocketStream, 30 | host: str, 31 | port: int, 32 | ) -> socks5.ConnectReply: 33 | conn = socks5.Connection() 34 | 35 | # Auth methods 36 | request = socks5.AuthMethodsRequest( 37 | username=self._username, 38 | password=self._password, 39 | ) 40 | data = conn.send(request) 41 | await stream.write_all(data) 42 | 43 | data = await stream.read_exact(socks5.AuthMethodReply.SIZE) 44 | reply: socks5.AuthMethodReply = conn.receive(data) 45 | 46 | # Authenticate 47 | if reply.method == socks5.AuthMethod.USERNAME_PASSWORD: 48 | request = socks5.AuthRequest( 49 | username=self._username, 50 | password=self._password, 51 | ) 52 | data = conn.send(request) 53 | await stream.write_all(data) 54 | 55 | data = await stream.read_exact(socks5.AuthReply.SIZE) 56 | _: socks5.AuthReply = conn.receive(data) 57 | 58 | # Connect 59 | if not is_ip_address(host) and not self._rdns: 60 | _, host = await self._resolver.resolve( 61 | host, 62 | family=socket.AF_UNSPEC, 63 | ) 64 | 65 | request = socks5.ConnectRequest(host=host, port=port) 66 | data = conn.send(request) 67 | await stream.write_all(data) 68 | 69 | data = await self._read_reply(stream) 70 | reply: socks5.ConnectReply = conn.receive(data) 71 | return reply 72 | 73 | # noinspection PyMethodMayBeStatic 74 | async def _read_reply(self, stream: AsyncSocketStream) -> bytes: 75 | data = await stream.read_exact(4) 76 | if data[0] != socks5.SOCKS_VER: 77 | return data 78 | if data[1] != socks5.ReplyCode.SUCCEEDED: 79 | return data 80 | if data[2] != socks5.RSV: 81 | return data 82 | 83 | addr_type = data[3] 84 | 85 | if addr_type == socks5.AddressType.IPV4: 86 | data += await stream.read_exact(6) 87 | elif addr_type == socks5.AddressType.IPV6: 88 | data += await stream.read_exact(18) 89 | elif addr_type == socks5.AddressType.DOMAIN: 90 | data += await stream.read_exact(1) 91 | host_len = data[-1] 92 | data += await stream.read_exact(host_len + 2) 93 | 94 | return data 95 | -------------------------------------------------------------------------------- /tests/test_resolvers.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from unittest.mock import MagicMock, patch 3 | 4 | import pytest 5 | 6 | from python_socks.async_.asyncio._resolver import Resolver as AsyncioResolver 7 | from python_socks.sync._resolver import SyncResolver 8 | 9 | RET_FAMILY = socket.AF_INET 10 | RET_HOST = '127.0.0.1' 11 | 12 | RET_VALUE = [( 13 | RET_FAMILY, 14 | socket.SOCK_STREAM, 15 | 6, 16 | '', 17 | (RET_HOST, 0) 18 | )] 19 | 20 | 21 | async def get_value_async(): 22 | return RET_VALUE 23 | 24 | 25 | TEST_HOST_NAME = 'fake.host.name' 26 | 27 | 28 | @patch('socket.getaddrinfo', return_value=RET_VALUE) 29 | def test_sync_resolver_1(_): 30 | resolver = SyncResolver() 31 | family, host = resolver.resolve(host=TEST_HOST_NAME) 32 | assert family == RET_FAMILY 33 | assert host == RET_HOST 34 | 35 | 36 | @patch('socket.getaddrinfo', return_value=[]) 37 | def test_sync_resolver_2(_): 38 | with pytest.raises(OSError): 39 | resolver = SyncResolver() 40 | resolver.resolve(host=TEST_HOST_NAME) 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_asyncio_resolver(): 45 | loop = MagicMock() 46 | loop.getaddrinfo = MagicMock() 47 | loop.getaddrinfo.return_value = get_value_async() 48 | resolver = AsyncioResolver(loop) 49 | family, host = await resolver.resolve(host=TEST_HOST_NAME) 50 | assert family == RET_FAMILY 51 | assert host == RET_HOST 52 | 53 | 54 | @pytest.mark.trio 55 | async def test_trio_resolver(): 56 | pytest.importorskip('trio') 57 | from python_socks.async_.trio._resolver import Resolver as TrioResolver 58 | 59 | getaddrinfo = MagicMock() 60 | getaddrinfo.return_value = get_value_async() 61 | # with patch('trio.socket.getaddrinfo', return_value=get_value_async()): 62 | with patch('trio.socket.getaddrinfo', new=getaddrinfo): 63 | resolver = TrioResolver() 64 | family, host = await resolver.resolve(host=TEST_HOST_NAME) 65 | assert family == RET_FAMILY 66 | assert host == RET_HOST 67 | 68 | 69 | @pytest.mark.anyio 70 | async def test_anyio_resolver(): 71 | pytest.importorskip('anyio') 72 | from python_socks.async_.anyio._resolver import Resolver as AnyioResolver 73 | 74 | getaddrinfo = MagicMock() 75 | getaddrinfo.return_value = get_value_async() 76 | with patch('anyio.getaddrinfo', new=getaddrinfo): 77 | resolver = AnyioResolver() 78 | family, host = await resolver.resolve(host=TEST_HOST_NAME) 79 | assert family == RET_FAMILY 80 | assert host == RET_HOST 81 | 82 | 83 | def test_curio_resolver(): 84 | curio = pytest.importorskip('curio') 85 | from python_socks.async_.curio._resolver import Resolver as CurioResolver 86 | 87 | getaddrinfo = MagicMock() 88 | getaddrinfo.return_value = get_value_async() 89 | to_patch = 'python_socks.async_.curio._resolver.getaddrinfo' 90 | 91 | async def run(): 92 | # with patch(to_patch, return_value=get_value_async()): 93 | with patch(to_patch, new=getaddrinfo): 94 | resolver = CurioResolver() 95 | family, host = await resolver.resolve(host=TEST_HOST_NAME) 96 | assert family == RET_FAMILY 97 | assert host == RET_HOST 98 | 99 | curio.run(run) 100 | -------------------------------------------------------------------------------- /python_socks/_protocols/socks4.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import ipaddress 3 | import socket 4 | from dataclasses import dataclass 5 | from typing import Optional 6 | 7 | from .errors import ReplyError 8 | from .._helpers import is_ipv4_address 9 | 10 | RSV = NULL = 0x00 11 | SOCKS_VER = 0x04 12 | 13 | 14 | class Command(enum.IntEnum): 15 | CONNECT = 0x01 16 | BIND = 0x02 17 | 18 | 19 | class ReplyCode(enum.IntEnum): 20 | REQUEST_GRANTED = 0x5A 21 | REQUEST_REJECTED_OR_FAILED = 0x5B 22 | CONNECTION_FAILED = 0x5C 23 | AUTHENTICATION_FAILED = 0x5D 24 | 25 | 26 | ReplyMessages = { 27 | ReplyCode.REQUEST_GRANTED: 'Request granted', 28 | ReplyCode.REQUEST_REJECTED_OR_FAILED: 'Request rejected or failed', 29 | ReplyCode.CONNECTION_FAILED: ( 30 | 'Request rejected because SOCKS server cannot connect to identd on the client' 31 | ), 32 | ReplyCode.AUTHENTICATION_FAILED: ( 33 | 'Request rejected because the client program and identd report different user-ids' 34 | ), 35 | } 36 | 37 | 38 | @dataclass 39 | class ConnectRequest: 40 | host: str # hostname or IPv4 address 41 | port: int 42 | user_id: Optional[str] 43 | 44 | def dumps(self): 45 | port_bytes = self.port.to_bytes(2, 'big') 46 | include_hostname = False 47 | 48 | if is_ipv4_address(self.host): 49 | host_bytes = ipaddress.IPv4Address(self.host).packed 50 | else: 51 | include_hostname = True 52 | host_bytes = bytes([NULL, NULL, NULL, 1]) 53 | 54 | data = bytearray([SOCKS_VER, Command.CONNECT]) 55 | data += port_bytes 56 | data += host_bytes 57 | 58 | if self.user_id: 59 | data += self.user_id.encode('ascii') 60 | 61 | data.append(NULL) 62 | 63 | if include_hostname: 64 | data += self.host.encode('idna') 65 | data.append(NULL) 66 | 67 | return bytes(data) 68 | 69 | 70 | @dataclass 71 | class ConnectReply: 72 | SIZE = 8 73 | 74 | rsv: int 75 | reply: ReplyCode 76 | host: str # should be ignored when using Command.CONNECT 77 | port: int # should be ignored when using Command.CONNECT 78 | 79 | @classmethod 80 | def loads(cls, data: bytes) -> 'ConnectReply': 81 | if len(data) != cls.SIZE: 82 | raise ReplyError('Malformed connect reply') 83 | 84 | rsv = data[0] 85 | if rsv != RSV: # pragma: no cover 86 | raise ReplyError(f'Unexpected reply version: {data[0]:#02X}') 87 | 88 | try: 89 | reply = ReplyCode(data[1]) 90 | except ValueError: 91 | raise ReplyError(f'Invalid reply code: {data[1]:#02X}') 92 | 93 | if reply != ReplyCode.REQUEST_GRANTED: # pragma: no cover 94 | msg = ReplyMessages.get(reply, 'Unknown error') 95 | raise ReplyError(msg, error_code=reply) 96 | 97 | try: 98 | port = int.from_bytes(data[2:4], byteorder="big") 99 | except ValueError: 100 | raise ReplyError('Invalid port data') 101 | 102 | try: 103 | host = socket.inet_ntop(socket.AF_INET, data[4:8]) 104 | except ValueError: 105 | raise ReplyError('Invalid port data') 106 | 107 | return cls(rsv=rsv, reply=reply, host=host, port=port) 108 | 109 | 110 | # noinspection PyMethodMayBeStatic 111 | class Connection: 112 | def send(self, request: ConnectRequest) -> bytes: 113 | return request.dumps() 114 | 115 | def receive(self, data: bytes) -> ConnectReply: 116 | return ConnectReply.loads(data) 117 | -------------------------------------------------------------------------------- /python_socks/sync/_proxy.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Optional, Any 3 | import warnings 4 | 5 | from .._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 6 | 7 | from .._types import ProxyType 8 | from .._helpers import parse_proxy_url 9 | from .._protocols.errors import ReplyError 10 | from .._connectors.factory_sync import create_connector 11 | 12 | from ._stream import SyncSocketStream 13 | from ._resolver import SyncResolver 14 | from ._connect import connect_tcp 15 | 16 | 17 | DEFAULT_TIMEOUT = 60 18 | 19 | 20 | class SyncProxy: 21 | def __init__( 22 | self, 23 | proxy_type: ProxyType, 24 | host: str, 25 | port: int, 26 | username: Optional[str] = None, 27 | password: Optional[str] = None, 28 | rdns: Optional[bool] = None, 29 | ): 30 | self._proxy_type = proxy_type 31 | self._proxy_host = host 32 | self._proxy_port = port 33 | self._password = password 34 | self._username = username 35 | self._rdns = rdns 36 | 37 | self._resolver = SyncResolver() 38 | 39 | def connect( 40 | self, 41 | dest_host: str, 42 | dest_port: int, 43 | timeout: Optional[float] = None, 44 | **kwargs: Any, 45 | ) -> socket.socket: 46 | if timeout is None: 47 | timeout = DEFAULT_TIMEOUT 48 | 49 | _socket = kwargs.get('_socket') 50 | if _socket is not None: 51 | warnings.warn( 52 | "The '_socket' argument is deprecated and will be removed in the future", 53 | DeprecationWarning, 54 | stacklevel=2, 55 | ) 56 | 57 | if _socket is None: 58 | local_addr = kwargs.get('local_addr') 59 | try: 60 | _socket = connect_tcp( 61 | host=self._proxy_host, 62 | port=self._proxy_port, 63 | timeout=timeout, 64 | local_addr=local_addr, 65 | ) 66 | except OSError as e: 67 | msg = 'Could not connect to proxy {}:{} [{}]'.format( 68 | self._proxy_host, 69 | self._proxy_port, 70 | e.strerror, 71 | ) 72 | raise ProxyConnectionError(e.errno, msg) from e 73 | 74 | stream = SyncSocketStream(_socket) 75 | 76 | try: 77 | connector = create_connector( 78 | proxy_type=self._proxy_type, 79 | username=self._username, 80 | password=self._password, 81 | rdns=self._rdns, 82 | resolver=self._resolver, 83 | ) 84 | connector.connect( 85 | stream=stream, 86 | host=dest_host, 87 | port=dest_port, 88 | ) 89 | 90 | return _socket 91 | except socket.timeout as e: 92 | stream.close() 93 | raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e 94 | except ReplyError as e: 95 | stream.close() 96 | raise ProxyError(e, error_code=e.error_code) 97 | except Exception: 98 | stream.close() 99 | raise 100 | 101 | @property 102 | def proxy_host(self): 103 | return self._proxy_host 104 | 105 | @property 106 | def proxy_port(self): 107 | return self._proxy_port 108 | 109 | @classmethod 110 | def create(cls, *args, **kwargs): # for backward compatibility 111 | return cls(*args, **kwargs) 112 | 113 | @classmethod 114 | def from_url(cls, url: str, **kwargs) -> 'SyncProxy': 115 | url_args = parse_proxy_url(url) 116 | return cls(*url_args, **kwargs) 117 | -------------------------------------------------------------------------------- /tests/proxy_server.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | import typing 3 | from multiprocessing import Process 4 | from unittest import mock 5 | 6 | import anyio 7 | from anyio import create_tcp_listener 8 | from anyio.streams.tls import TLSListener 9 | from tiny_proxy import ( 10 | HttpProxyHandler, 11 | Socks5ProxyHandler, 12 | Socks4ProxyHandler, 13 | HttpProxy, 14 | Socks4Proxy, 15 | Socks5Proxy, 16 | AbstractProxy, 17 | ) 18 | 19 | from tests.mocks import getaddrinfo_async_mock 20 | 21 | 22 | class ProxyConfig(typing.NamedTuple): 23 | proxy_type: str 24 | host: str 25 | port: int 26 | username: typing.Optional[str] = None 27 | password: typing.Optional[str] = None 28 | ssl_certfile: typing.Optional[str] = None 29 | ssl_keyfile: typing.Optional[str] = None 30 | 31 | def to_dict(self): 32 | d = {} 33 | for key, val in self._asdict().items(): 34 | if val is not None: 35 | d[key] = val 36 | return d 37 | 38 | 39 | cls_map = { 40 | 'http': HttpProxyHandler, 41 | 'socks4': Socks4ProxyHandler, 42 | 'socks5': Socks5ProxyHandler, 43 | } 44 | 45 | 46 | def connect_to_remote_factory(cls: typing.Type[AbstractProxy]): 47 | """ 48 | simulate target host connection timeout 49 | """ 50 | origin_connect_to_remote = cls.connect_to_remote 51 | 52 | async def new_connect_to_remote(self): 53 | await anyio.sleep(0.01) 54 | return await origin_connect_to_remote(self) 55 | 56 | return new_connect_to_remote 57 | 58 | 59 | @mock.patch.object( 60 | HttpProxy, 61 | attribute='connect_to_remote', 62 | new=connect_to_remote_factory(HttpProxy), 63 | ) 64 | @mock.patch.object( 65 | Socks4Proxy, 66 | attribute='connect_to_remote', 67 | new=connect_to_remote_factory(Socks4Proxy), 68 | ) 69 | @mock.patch.object( 70 | Socks5Proxy, 71 | attribute='connect_to_remote', 72 | new=connect_to_remote_factory(Socks5Proxy), 73 | ) 74 | @mock.patch('anyio._core._sockets.getaddrinfo', new=getaddrinfo_async_mock(anyio.getaddrinfo)) 75 | def start( 76 | proxy_type, 77 | host, 78 | port, 79 | ssl_certfile=None, 80 | ssl_keyfile=None, 81 | **kwargs, 82 | ): 83 | handler_cls = cls_map.get(proxy_type) 84 | if not handler_cls: 85 | raise RuntimeError(f'Unsupported type: {proxy_type}') 86 | 87 | if ssl_certfile and ssl_keyfile: 88 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 89 | ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) 90 | else: 91 | ssl_context = None 92 | 93 | print(f'Starting {proxy_type} proxy on {host}:{port}...') 94 | 95 | handler = handler_cls(**kwargs) 96 | 97 | async def serve(): 98 | listener = await create_tcp_listener(local_host=host, local_port=port) 99 | if ssl_context is not None: 100 | listener = TLSListener(listener=listener, ssl_context=ssl_context) 101 | 102 | async with listener: 103 | await listener.serve(handler.handle) 104 | 105 | anyio.run(serve) 106 | 107 | 108 | class ProxyServer: 109 | workers: typing.List[Process] 110 | 111 | def __init__(self, config: typing.Iterable[ProxyConfig]): 112 | self.config = config 113 | self.workers = [] 114 | 115 | def start(self): 116 | for cfg in self.config: 117 | print( 118 | 'Starting {} proxy on {}:{}; certfile={}, keyfile={}...'.format( 119 | cfg.proxy_type, 120 | cfg.host, 121 | cfg.port, 122 | cfg.ssl_certfile, 123 | cfg.ssl_keyfile, 124 | ) 125 | ) 126 | 127 | p = Process(target=start, kwargs=cfg.to_dict(), daemon=True) 128 | self.workers.append(p) 129 | 130 | for p in self.workers: 131 | p.start() 132 | 133 | def terminate(self): 134 | for p in self.workers: 135 | p.terminate() 136 | -------------------------------------------------------------------------------- /python_socks/sync/v2/_proxy.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import ssl 3 | from typing import Any, Optional 4 | 5 | from ._connect import connect_tcp 6 | from ._stream import SyncSocketStream 7 | from .._resolver import SyncResolver 8 | from ..._types import ProxyType 9 | from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 10 | from ..._helpers import parse_proxy_url 11 | 12 | from ..._protocols.errors import ReplyError 13 | from ..._connectors.factory_sync import create_connector 14 | 15 | 16 | DEFAULT_TIMEOUT = 60 17 | 18 | 19 | class SyncProxy: 20 | def __init__( 21 | self, 22 | proxy_type: ProxyType, 23 | host: str, 24 | port: int, 25 | username: Optional[str] = None, 26 | password: Optional[str] = None, 27 | rdns: Optional[bool] = None, 28 | proxy_ssl: Optional[ssl.SSLContext] = None, 29 | forward: Optional['SyncProxy'] = None, 30 | ): 31 | self._proxy_type = proxy_type 32 | self._proxy_host = host 33 | self._proxy_port = port 34 | self._username = username 35 | self._password = password 36 | self._rdns = rdns 37 | self._proxy_ssl = proxy_ssl 38 | self._forward = forward 39 | 40 | self._resolver = SyncResolver() 41 | 42 | def connect( 43 | self, 44 | dest_host: str, 45 | dest_port: int, 46 | dest_ssl: Optional[ssl.SSLContext] = None, 47 | timeout: Optional[float] = None, 48 | **kwargs: Any, 49 | ) -> SyncSocketStream: 50 | if timeout is None: 51 | timeout = DEFAULT_TIMEOUT 52 | 53 | if self._forward is None: 54 | local_addr = kwargs.get('local_addr') 55 | try: 56 | stream = connect_tcp( 57 | host=self._proxy_host, 58 | port=self._proxy_port, 59 | timeout=timeout, 60 | local_addr=local_addr, 61 | ) 62 | except OSError as e: 63 | msg = 'Could not connect to proxy {}:{} [{}]'.format( 64 | self._proxy_host, 65 | self._proxy_port, 66 | e.strerror, 67 | ) 68 | raise ProxyConnectionError(e.errno, msg) from e 69 | else: 70 | stream = self._forward.connect( 71 | dest_host=self._proxy_host, 72 | dest_port=self._proxy_port, 73 | timeout=timeout, 74 | ) 75 | 76 | try: 77 | if self._proxy_ssl is not None: 78 | stream = stream.start_tls( 79 | hostname=self._proxy_host, 80 | ssl_context=self._proxy_ssl, 81 | ) 82 | 83 | connector = create_connector( 84 | proxy_type=self._proxy_type, 85 | username=self._username, 86 | password=self._password, 87 | rdns=self._rdns, 88 | resolver=self._resolver, 89 | ) 90 | connector.connect( 91 | stream=stream, 92 | host=dest_host, 93 | port=dest_port, 94 | ) 95 | 96 | if dest_ssl is not None: 97 | stream = stream.start_tls( 98 | hostname=dest_host, 99 | ssl_context=dest_ssl, 100 | ) 101 | 102 | return stream 103 | 104 | except socket.timeout as e: 105 | stream.close() 106 | raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e 107 | except ReplyError as e: 108 | stream.close() 109 | raise ProxyError(e, error_code=e.error_code) 110 | except Exception: 111 | stream.close() 112 | raise 113 | 114 | @classmethod 115 | def create(cls, *args, **kwargs): # for backward compatibility 116 | return cls(*args, **kwargs) 117 | 118 | @classmethod 119 | def from_url(cls, url: str, **kwargs) -> 'SyncProxy': 120 | url_args = parse_proxy_url(url) 121 | return cls(*url_args, **kwargs) 122 | -------------------------------------------------------------------------------- /python_socks/async_/curio/_proxy.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | import warnings 3 | import curio 4 | import curio.io 5 | 6 | from ..._types import ProxyType 7 | from ..._helpers import parse_proxy_url 8 | from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 9 | 10 | from ._stream import CurioSocketStream 11 | from ._resolver import Resolver 12 | from ._connect import connect_tcp 13 | 14 | from ..._protocols.errors import ReplyError 15 | from ..._connectors.factory_async import create_connector 16 | 17 | 18 | DEFAULT_TIMEOUT = 60 19 | 20 | 21 | class CurioProxy: 22 | def __init__( 23 | self, 24 | proxy_type: ProxyType, 25 | host: str, 26 | port: int, 27 | username: Optional[str] = None, 28 | password: Optional[str] = None, 29 | rdns: Optional[bool] = None, 30 | ): 31 | self._proxy_type = proxy_type 32 | self._proxy_host = host 33 | self._proxy_port = port 34 | self._password = password 35 | self._username = username 36 | self._rdns = rdns 37 | 38 | self._resolver = Resolver() 39 | 40 | async def connect( 41 | self, 42 | dest_host: str, 43 | dest_port: int, 44 | timeout: Optional[float] = None, 45 | **kwargs: Any, 46 | ) -> curio.io.Socket: 47 | if timeout is None: 48 | timeout = DEFAULT_TIMEOUT 49 | 50 | _socket = kwargs.get('_socket') 51 | if _socket is not None: 52 | warnings.warn( 53 | "The '_socket' argument is deprecated and will be removed in the future", 54 | DeprecationWarning, 55 | stacklevel=2, 56 | ) 57 | 58 | local_addr = kwargs.get('local_addr') 59 | try: 60 | return await curio.timeout_after( 61 | timeout, 62 | self._connect, 63 | dest_host, 64 | dest_port, 65 | _socket, 66 | local_addr, 67 | ) 68 | except curio.TaskTimeout as e: 69 | raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e 70 | 71 | async def _connect( 72 | self, 73 | dest_host: str, 74 | dest_port: int, 75 | _socket=None, 76 | local_addr=None, 77 | ): 78 | if _socket is None: 79 | try: 80 | _socket = await connect_tcp( 81 | host=self._proxy_host, 82 | port=self._proxy_port, 83 | local_addr=local_addr, 84 | ) 85 | except OSError as e: 86 | msg = 'Could not connect to proxy {}:{} [{}]'.format( 87 | self._proxy_host, 88 | self._proxy_port, 89 | e.strerror, 90 | ) 91 | raise ProxyConnectionError(e.errno, msg) from e 92 | 93 | stream = CurioSocketStream(_socket) 94 | 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 | ) 108 | return _socket 109 | 110 | except ReplyError as e: 111 | await stream.close() 112 | raise ProxyError(e, error_code=e.error_code) 113 | except BaseException: 114 | await stream.close() 115 | raise 116 | 117 | @property 118 | def proxy_host(self): 119 | return self._proxy_host 120 | 121 | @property 122 | def proxy_port(self): 123 | return self._proxy_port 124 | 125 | @classmethod 126 | def create(cls, *args, **kwargs): # for backward compatibility 127 | return cls(*args, **kwargs) 128 | 129 | @classmethod 130 | def from_url(cls, url: str, **kwargs) -> 'CurioProxy': 131 | url_args = parse_proxy_url(url) 132 | return cls(*url_args, **kwargs) 133 | -------------------------------------------------------------------------------- /python_socks/async_/trio/_proxy.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | import warnings 3 | import trio 4 | 5 | from ..._types import ProxyType 6 | from ..._helpers import parse_proxy_url 7 | from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 8 | 9 | from ._stream import TrioSocketStream 10 | from ._resolver import Resolver 11 | from ._connect import connect_tcp 12 | 13 | from ..._protocols.errors import ReplyError 14 | from ..._connectors.factory_async import create_connector 15 | 16 | 17 | DEFAULT_TIMEOUT = 60 18 | 19 | 20 | class TrioProxy: 21 | def __init__( 22 | self, 23 | proxy_type: ProxyType, 24 | host: str, 25 | port: int, 26 | username: Optional[str] = None, 27 | password: Optional[str] = None, 28 | rdns: Optional[bool] = None, 29 | ): 30 | self._proxy_type = proxy_type 31 | self._proxy_host = host 32 | self._proxy_port = port 33 | self._password = password 34 | self._username = username 35 | self._rdns = rdns 36 | 37 | self._resolver = Resolver() 38 | 39 | async def connect( 40 | self, 41 | dest_host: str, 42 | dest_port: int, 43 | timeout: Optional[float] = None, 44 | **kwargs: Any, 45 | ) -> trio.socket.SocketType: 46 | if timeout is None: 47 | timeout = DEFAULT_TIMEOUT 48 | 49 | _socket = kwargs.get('_socket') 50 | if _socket is not None: 51 | warnings.warn( 52 | "The '_socket' argument is deprecated and will be removed in the future", 53 | DeprecationWarning, 54 | stacklevel=2, 55 | ) 56 | 57 | local_addr = kwargs.get('local_addr') 58 | try: 59 | with trio.fail_after(timeout): 60 | return await self._connect( 61 | dest_host=dest_host, 62 | dest_port=dest_port, 63 | _socket=_socket, 64 | local_addr=local_addr, 65 | ) 66 | except trio.TooSlowError as e: 67 | raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e 68 | 69 | async def _connect( 70 | self, 71 | dest_host: str, 72 | dest_port: int, 73 | _socket=None, 74 | local_addr=None, 75 | ) -> trio.socket.SocketType: 76 | if _socket is None: 77 | try: 78 | _socket = await connect_tcp( 79 | host=self._proxy_host, 80 | port=self._proxy_port, 81 | local_addr=local_addr, 82 | ) 83 | except OSError as e: 84 | msg = 'Could not connect to proxy {}:{} [{}]'.format( 85 | self._proxy_host, 86 | self._proxy_port, 87 | e.strerror, 88 | ) 89 | raise ProxyConnectionError(e.errno, msg) from e 90 | 91 | stream = TrioSocketStream(sock=_socket) 92 | 93 | try: 94 | connector = create_connector( 95 | proxy_type=self._proxy_type, 96 | username=self._username, 97 | password=self._password, 98 | rdns=self._rdns, 99 | resolver=self._resolver, 100 | ) 101 | await connector.connect( 102 | stream=stream, 103 | host=dest_host, 104 | port=dest_port, 105 | ) 106 | return _socket 107 | 108 | except ReplyError as e: 109 | await stream.close() 110 | raise ProxyError(e, error_code=e.error_code) 111 | except BaseException: # trio.Cancelled... 112 | with trio.CancelScope(shield=True): 113 | await stream.close() 114 | raise 115 | 116 | @property 117 | def proxy_host(self): 118 | return self._proxy_host 119 | 120 | @property 121 | def proxy_port(self): 122 | return self._proxy_port 123 | 124 | @classmethod 125 | def create(cls, *args, **kwargs): # for backward compatibility 126 | return cls(*args, **kwargs) 127 | 128 | @classmethod 129 | def from_url(cls, url: str, **kwargs) -> 'TrioProxy': 130 | url_args = parse_proxy_url(url) 131 | return cls(*url_args, **kwargs) 132 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/v2/_proxy.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Any, Optional 3 | 4 | import anyio 5 | 6 | from ._connect import connect_tcp 7 | from ._stream import AnyioSocketStream 8 | from .._resolver import Resolver 9 | from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 10 | 11 | from ...._types import ProxyType 12 | from ...._helpers import parse_proxy_url 13 | 14 | from ...._protocols.errors import ReplyError 15 | from ...._connectors.factory_async import create_connector 16 | 17 | DEFAULT_TIMEOUT = 60 18 | 19 | 20 | class AnyioProxy: 21 | def __init__( 22 | self, 23 | proxy_type: ProxyType, 24 | host: str, 25 | port: int, 26 | username: Optional[str] = None, 27 | password: Optional[str] = None, 28 | rdns: Optional[bool] = None, 29 | proxy_ssl: Optional[ssl.SSLContext] = None, 30 | forward: Optional['AnyioProxy'] = None, 31 | ): 32 | self._proxy_type = proxy_type 33 | self._proxy_host = host 34 | self._proxy_port = port 35 | self._username = username 36 | self._password = password 37 | self._rdns = rdns 38 | 39 | self._proxy_ssl = proxy_ssl 40 | self._forward = forward 41 | 42 | self._resolver = Resolver() 43 | 44 | async def connect( 45 | self, 46 | dest_host: str, 47 | dest_port: int, 48 | dest_ssl: Optional[ssl.SSLContext] = None, 49 | timeout: Optional[float] = None, 50 | **kwargs: Any, 51 | ) -> AnyioSocketStream: 52 | if timeout is None: 53 | timeout = DEFAULT_TIMEOUT 54 | 55 | local_host = kwargs.get('local_host') 56 | try: 57 | with anyio.fail_after(timeout): 58 | return await self._connect( 59 | dest_host=dest_host, 60 | dest_port=dest_port, 61 | dest_ssl=dest_ssl, 62 | local_host=local_host, 63 | ) 64 | except TimeoutError as e: 65 | raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e 66 | 67 | async def _connect( 68 | self, 69 | dest_host: str, 70 | dest_port: int, 71 | dest_ssl: Optional[ssl.SSLContext] = None, 72 | local_host: Optional[str] = None, 73 | ) -> AnyioSocketStream: 74 | if self._forward is None: 75 | try: 76 | stream = await connect_tcp( 77 | host=self._proxy_host, 78 | port=self._proxy_port, 79 | local_host=local_host, 80 | ) 81 | except OSError as e: 82 | raise ProxyConnectionError( 83 | e.errno, 84 | "Couldn't connect to proxy" 85 | f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]", 86 | ) from e 87 | else: 88 | stream = await self._forward.connect( 89 | dest_host=self._proxy_host, 90 | dest_port=self._proxy_port, 91 | ) 92 | 93 | try: 94 | if self._proxy_ssl is not None: 95 | stream = await stream.start_tls( 96 | hostname=self._proxy_host, 97 | ssl_context=self._proxy_ssl, 98 | ) 99 | 100 | connector = create_connector( 101 | proxy_type=self._proxy_type, 102 | username=self._username, 103 | password=self._password, 104 | rdns=self._rdns, 105 | resolver=self._resolver, 106 | ) 107 | await connector.connect( 108 | stream=stream, 109 | host=dest_host, 110 | port=dest_port, 111 | ) 112 | 113 | if dest_ssl is not None: 114 | stream = await stream.start_tls( 115 | hostname=dest_host, 116 | ssl_context=dest_ssl, 117 | ) 118 | except ReplyError as e: 119 | await stream.close() 120 | raise ProxyError(e, error_code=e.error_code) 121 | except BaseException: 122 | with anyio.CancelScope(shield=True): 123 | await stream.close() 124 | raise 125 | 126 | return stream 127 | 128 | @classmethod 129 | def create(cls, *args, **kwargs): # for backward compatibility 130 | return cls(*args, **kwargs) 131 | 132 | @classmethod 133 | def from_url(cls, url: str, **kwargs) -> 'AnyioProxy': 134 | url_args = parse_proxy_url(url) 135 | return cls(*url_args, **kwargs) 136 | -------------------------------------------------------------------------------- /python_socks/async_/trio/v2/_proxy.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Any, Optional 3 | 4 | import trio 5 | 6 | from ._connect import connect_tcp 7 | from ._stream import TrioSocketStream 8 | from .._resolver import Resolver 9 | 10 | from ...._types import ProxyType 11 | from ...._helpers import parse_proxy_url 12 | from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 13 | 14 | from ...._protocols.errors import ReplyError 15 | from ...._connectors.factory_async import create_connector 16 | 17 | DEFAULT_TIMEOUT = 60 18 | 19 | 20 | class TrioProxy: 21 | def __init__( 22 | self, 23 | proxy_type: ProxyType, 24 | host: str, 25 | port: int, 26 | username: Optional[str] = None, 27 | password: Optional[str] = None, 28 | rdns: Optional[bool] = None, 29 | proxy_ssl: Optional[ssl.SSLContext] = None, 30 | forward: Optional['TrioProxy'] = None, 31 | ): 32 | self._proxy_type = proxy_type 33 | self._proxy_host = host 34 | self._proxy_port = port 35 | self._username = username 36 | self._password = password 37 | self._rdns = rdns 38 | 39 | self._proxy_ssl = proxy_ssl 40 | self._forward = forward 41 | 42 | self._resolver = Resolver() 43 | 44 | async def connect( 45 | self, 46 | dest_host: str, 47 | dest_port: int, 48 | dest_ssl: Optional[ssl.SSLContext] = None, 49 | timeout: Optional[float] = None, 50 | **kwargs: Any, 51 | ) -> TrioSocketStream: 52 | if timeout is None: 53 | timeout = DEFAULT_TIMEOUT 54 | 55 | local_addr = kwargs.get('local_addr') 56 | try: 57 | with trio.fail_after(timeout): 58 | return await self._connect( 59 | dest_host=dest_host, 60 | dest_port=dest_port, 61 | dest_ssl=dest_ssl, 62 | local_addr=local_addr, 63 | ) 64 | except trio.TooSlowError as e: 65 | raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e 66 | 67 | async def _connect( 68 | self, 69 | dest_host: str, 70 | dest_port: int, 71 | dest_ssl: Optional[ssl.SSLContext] = None, 72 | local_addr: Optional[str] = None, 73 | ) -> TrioSocketStream: 74 | if self._forward is None: 75 | try: 76 | stream = await connect_tcp( 77 | host=self._proxy_host, 78 | port=self._proxy_port, 79 | local_addr=local_addr, 80 | ) 81 | except OSError as e: 82 | raise ProxyConnectionError( 83 | e.errno, 84 | "Couldn't connect to proxy" 85 | f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]", 86 | ) from e 87 | else: 88 | stream = await self._forward.connect( 89 | dest_host=self._proxy_host, 90 | dest_port=self._proxy_port, 91 | ) 92 | 93 | try: 94 | if self._proxy_ssl is not None: 95 | stream = await stream.start_tls( 96 | hostname=self._proxy_host, 97 | ssl_context=self._proxy_ssl, 98 | ) 99 | 100 | connector = create_connector( 101 | proxy_type=self._proxy_type, 102 | username=self._username, 103 | password=self._password, 104 | rdns=self._rdns, 105 | resolver=self._resolver, 106 | ) 107 | await connector.connect( 108 | stream=stream, 109 | host=dest_host, 110 | port=dest_port, 111 | ) 112 | 113 | if dest_ssl is not None: 114 | stream = await stream.start_tls( 115 | hostname=dest_host, 116 | ssl_context=dest_ssl, 117 | ) 118 | except ReplyError as e: 119 | await stream.close() 120 | raise ProxyError(e, error_code=e.error_code) 121 | except BaseException: # trio.Cancelled... 122 | with trio.CancelScope(shield=True): 123 | await stream.close() 124 | raise 125 | 126 | return stream 127 | 128 | @classmethod 129 | def create(cls, *args, **kwargs): # for backward compatibility 130 | return cls(*args, **kwargs) 131 | 132 | @classmethod 133 | def from_url(cls, url: str, **kwargs) -> 'TrioProxy': 134 | url_args = parse_proxy_url(url) 135 | return cls(*url_args, **kwargs) 136 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/_proxy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import sys 4 | from typing import Any, Optional 5 | import warnings 6 | 7 | from ..._types import ProxyType 8 | from ..._helpers import parse_proxy_url 9 | from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 10 | from ._stream import AsyncioSocketStream 11 | from ._resolver import Resolver 12 | 13 | from ..._protocols.errors import ReplyError 14 | from ..._connectors.factory_async import create_connector 15 | 16 | from ._connect import connect_tcp 17 | 18 | if sys.version_info >= (3, 11): 19 | import asyncio as async_timeout # pylint:disable=reimported 20 | else: 21 | import async_timeout 22 | 23 | DEFAULT_TIMEOUT = 60 24 | 25 | 26 | class AsyncioProxy: 27 | def __init__( 28 | self, 29 | proxy_type: ProxyType, 30 | host: str, 31 | port: int, 32 | username: Optional[str] = None, 33 | password: Optional[str] = None, 34 | rdns: Optional[bool] = None, 35 | loop: Optional[asyncio.AbstractEventLoop] = None, 36 | ): 37 | if loop is None: 38 | loop = asyncio.get_event_loop() 39 | 40 | self._loop = loop 41 | 42 | self._proxy_type = proxy_type 43 | self._proxy_host = host 44 | self._proxy_port = port 45 | self._password = password 46 | self._username = username 47 | self._rdns = rdns 48 | 49 | self._resolver = Resolver(loop=loop) 50 | 51 | async def connect( 52 | self, 53 | dest_host: str, 54 | dest_port: int, 55 | timeout: Optional[float] = None, 56 | **kwargs: Any, 57 | ) -> socket.socket: 58 | if timeout is None: 59 | timeout = DEFAULT_TIMEOUT 60 | 61 | _socket = kwargs.get('_socket') 62 | if _socket is not None: 63 | warnings.warn( 64 | "The '_socket' argument is deprecated and will be removed in the future", 65 | DeprecationWarning, 66 | stacklevel=2, 67 | ) 68 | 69 | local_addr = kwargs.get('local_addr') 70 | try: 71 | async with async_timeout.timeout(timeout): 72 | return await self._connect( 73 | dest_host=dest_host, 74 | dest_port=dest_port, 75 | _socket=_socket, 76 | local_addr=local_addr, 77 | ) 78 | except asyncio.TimeoutError as e: 79 | raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e 80 | 81 | async def _connect( 82 | self, 83 | dest_host, 84 | dest_port, 85 | _socket=None, 86 | local_addr=None, 87 | ) -> socket.socket: 88 | if _socket is None: 89 | try: 90 | _socket = await connect_tcp( 91 | host=self._proxy_host, 92 | port=self._proxy_port, 93 | loop=self._loop, 94 | local_addr=local_addr, 95 | ) 96 | except OSError as e: 97 | msg = 'Could not connect to proxy {}:{} [{}]'.format( 98 | self._proxy_host, 99 | self._proxy_port, 100 | e.strerror, 101 | ) 102 | raise ProxyConnectionError(e.errno, msg) from e 103 | 104 | stream = AsyncioSocketStream(sock=_socket, loop=self._loop) 105 | 106 | try: 107 | connector = create_connector( 108 | proxy_type=self._proxy_type, 109 | username=self._username, 110 | password=self._password, 111 | rdns=self._rdns, 112 | resolver=self._resolver, 113 | ) 114 | await connector.connect( 115 | stream=stream, 116 | host=dest_host, 117 | port=dest_port, 118 | ) 119 | 120 | return _socket 121 | except ReplyError as e: 122 | await stream.close() 123 | raise ProxyError(e, error_code=e.error_code) 124 | except (asyncio.CancelledError, Exception): # pragma: no cover 125 | await stream.close() 126 | raise 127 | 128 | @property 129 | def proxy_host(self): 130 | return self._proxy_host 131 | 132 | @property 133 | def proxy_port(self): 134 | return self._proxy_port 135 | 136 | @classmethod 137 | def create(cls, *args, **kwargs): # for backward compatibility 138 | return cls(*args, **kwargs) 139 | 140 | @classmethod 141 | def from_url(cls, url: str, **kwargs) -> 'AsyncioProxy': 142 | url_args = parse_proxy_url(url) 143 | return cls(*url_args, **kwargs) 144 | -------------------------------------------------------------------------------- /python_socks/async_/anyio/_proxy.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from typing import Any, Optional 3 | import warnings 4 | 5 | import anyio 6 | 7 | from ..._types import ProxyType 8 | from ..._helpers import parse_proxy_url 9 | from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 10 | 11 | from ._resolver import Resolver 12 | 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 | 18 | DEFAULT_TIMEOUT = 60 19 | 20 | 21 | class AnyioProxy: 22 | _stream: Optional[AnyioSocketStream] 23 | 24 | def __init__( 25 | self, 26 | proxy_type: ProxyType, 27 | host: str, 28 | port: int, 29 | username: Optional[str] = None, 30 | password: Optional[str] = None, 31 | rdns: Optional[bool] = None, 32 | proxy_ssl: Optional[ssl.SSLContext] = None, 33 | ): 34 | self._proxy_type = proxy_type 35 | self._proxy_host = host 36 | self._proxy_port = port 37 | self._password = password 38 | self._username = username 39 | self._rdns = rdns 40 | 41 | self._proxy_ssl = proxy_ssl 42 | self._resolver = Resolver() 43 | 44 | async def connect( 45 | self, 46 | dest_host: str, 47 | dest_port: int, 48 | dest_ssl: Optional[ssl.SSLContext] = None, 49 | timeout: Optional[float] = None, 50 | **kwargs: Any, 51 | ) -> AnyioSocketStream: 52 | if timeout is None: 53 | timeout = DEFAULT_TIMEOUT 54 | 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 | try: 65 | with anyio.fail_after(timeout): 66 | if _stream is None: 67 | try: 68 | _stream = AnyioSocketStream( 69 | await connect_tcp( 70 | host=self._proxy_host, 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 | 119 | except TimeoutError as e: 120 | raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e 121 | 122 | @property 123 | def proxy_host(self): 124 | return self._proxy_host 125 | 126 | @property 127 | def proxy_port(self): 128 | return self._proxy_port 129 | 130 | @classmethod 131 | def create(cls, *args, **kwargs): # for backward compatibility 132 | return cls(*args, **kwargs) 133 | 134 | @classmethod 135 | def from_url(cls, url: str, **kwargs) -> 'AnyioProxy': 136 | url_args = parse_proxy_url(url) 137 | return cls(*url_args, **kwargs) 138 | -------------------------------------------------------------------------------- /python_socks/_protocols/http.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass 3 | import base64 4 | import binascii 5 | from collections import namedtuple 6 | from typing import Optional 7 | 8 | from .._version import __title__, __version__ 9 | 10 | from .errors import ReplyError 11 | 12 | DEFAULT_USER_AGENT = 'Python/{0[0]}.{0[1]} {1}/{2}'.format( 13 | sys.version_info, 14 | __title__, 15 | __version__, 16 | ) 17 | 18 | CRLF = '\r\n' 19 | 20 | 21 | class BasicAuth(namedtuple('BasicAuth', ['login', 'password', 'encoding'])): 22 | """Http basic authentication helper.""" 23 | 24 | def __new__(cls, login: str, password: str = '', encoding: str = 'latin1') -> 'BasicAuth': 25 | if login is None: 26 | raise ValueError('None is not allowed as login value') 27 | 28 | if password is None: 29 | raise ValueError('None is not allowed as password value') 30 | 31 | if ':' in login: 32 | raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)') 33 | 34 | # noinspection PyTypeChecker,PyArgumentList 35 | return super().__new__(cls, login, password, encoding) 36 | 37 | @classmethod 38 | def decode(cls, auth_header: str, encoding: str = 'latin1') -> 'BasicAuth': 39 | """Create a BasicAuth object from an Authorization HTTP header.""" 40 | try: 41 | auth_type, encoded_credentials = auth_header.split(' ', 1) 42 | except ValueError: 43 | raise ValueError('Could not parse authorization header.') 44 | 45 | if auth_type.lower() != 'basic': 46 | raise ValueError('Unknown authorization method %s' % auth_type) 47 | 48 | try: 49 | decoded = base64.b64decode(encoded_credentials.encode('ascii'), validate=True).decode( 50 | encoding 51 | ) 52 | except binascii.Error: 53 | raise ValueError('Invalid base64 encoding.') 54 | 55 | try: 56 | # RFC 2617 HTTP Authentication 57 | # https://www.ietf.org/rfc/rfc2617.txt 58 | # the colon must be present, but the username and password may be 59 | # otherwise blank. 60 | username, password = decoded.split(':', 1) 61 | except ValueError: 62 | raise ValueError('Invalid credentials.') 63 | 64 | # noinspection PyTypeChecker 65 | return cls(username, password, encoding=encoding) 66 | 67 | def encode(self) -> str: 68 | """Encode credentials.""" 69 | creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding) 70 | return 'Basic %s' % base64.b64encode(creds).decode(self.encoding) 71 | 72 | 73 | class _Buffer: 74 | def __init__(self, encoding: str = 'utf-8'): 75 | self._encoding = encoding 76 | self._buffer = bytearray() 77 | 78 | def append_line(self, line: str = ""): 79 | if line: 80 | self._buffer.extend(line.encode(self._encoding)) 81 | 82 | self._buffer.extend(CRLF.encode('ascii')) 83 | 84 | def dumps(self) -> bytes: 85 | return bytes(self._buffer) 86 | 87 | 88 | @dataclass 89 | class ConnectRequest: 90 | host: str 91 | port: int 92 | username: Optional[str] 93 | password: Optional[str] 94 | 95 | def dumps(self) -> bytes: 96 | buff = _Buffer() 97 | buff.append_line(f'CONNECT {self.host}:{self.port} HTTP/1.1') 98 | buff.append_line(f'Host: {self.host}:{self.port}') 99 | buff.append_line(f'User-Agent: {DEFAULT_USER_AGENT}') 100 | 101 | if self.username and self.password: 102 | auth = BasicAuth(self.username, self.password) 103 | buff.append_line(f'Proxy-Authorization: {auth.encode()}') 104 | 105 | buff.append_line() 106 | 107 | return buff.dumps() 108 | 109 | 110 | @dataclass 111 | class ConnectReply: 112 | status_code: int 113 | message: str 114 | 115 | @classmethod 116 | def loads(cls, data: bytes) -> 'ConnectReply': 117 | if not data: 118 | raise ReplyError('Invalid proxy response') # pragma: no cover 119 | 120 | line = data.split(CRLF.encode('ascii'), 1)[0] 121 | line = line.decode('utf-8', 'surrogateescape') 122 | 123 | try: 124 | version, code, *reason = line.split() 125 | except ValueError: # pragma: no cover 126 | raise ReplyError(f'Invalid status line: {line}') 127 | 128 | try: 129 | status_code = int(code) 130 | except ValueError: # pragma: no cover 131 | raise ReplyError(f'Invalid status code: {code}') 132 | 133 | status_message = " ".join(reason) 134 | 135 | if status_code != 200: 136 | msg = f'{status_code} {status_message}' 137 | raise ReplyError(msg, error_code=status_code) 138 | 139 | return cls(status_code=status_code, message=status_message) 140 | 141 | 142 | # noinspection PyMethodMayBeStatic 143 | class Connection: 144 | def send(self, request: ConnectRequest) -> bytes: 145 | return request.dumps() 146 | 147 | def receive(self, data: bytes) -> ConnectReply: 148 | return ConnectReply.loads(data) 149 | -------------------------------------------------------------------------------- /python_socks/async_/asyncio/v2/_proxy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import ssl 3 | from typing import Any, Optional, Tuple 4 | import warnings 5 | import sys 6 | 7 | 8 | from ...._types import ProxyType 9 | from ...._helpers import parse_proxy_url 10 | from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError 11 | 12 | from ...._protocols.errors import ReplyError 13 | from ...._connectors.factory_async import create_connector 14 | 15 | from .._resolver import Resolver 16 | from ._stream import AsyncioSocketStream 17 | from ._connect import connect_tcp 18 | 19 | if sys.version_info >= (3, 11): 20 | import asyncio as async_timeout # pylint:disable=reimported 21 | else: 22 | import async_timeout 23 | 24 | 25 | DEFAULT_TIMEOUT = 60 26 | 27 | 28 | class AsyncioProxy: 29 | def __init__( 30 | self, 31 | proxy_type: ProxyType, 32 | host: str, 33 | port: int, 34 | username: Optional[str] = None, 35 | password: Optional[str] = None, 36 | rdns: Optional[bool] = None, 37 | proxy_ssl: Optional[ssl.SSLContext] = None, 38 | forward: Optional['AsyncioProxy'] = None, 39 | loop: Optional[asyncio.AbstractEventLoop] = None, 40 | ): 41 | if loop is not None: # pragma: no cover 42 | warnings.warn( 43 | 'The loop argument is deprecated and scheduled for removal in the future.', 44 | DeprecationWarning, 45 | stacklevel=2, 46 | ) 47 | 48 | if loop is None: 49 | loop = asyncio.get_event_loop() 50 | 51 | self._loop = loop 52 | 53 | self._proxy_type = proxy_type 54 | self._proxy_host = host 55 | self._proxy_port = port 56 | self._username = username 57 | self._password = password 58 | self._rdns = rdns 59 | 60 | self._proxy_ssl = proxy_ssl 61 | self._forward = forward 62 | 63 | self._resolver = Resolver(loop=loop) 64 | 65 | async def connect( 66 | self, 67 | dest_host: str, 68 | dest_port: int, 69 | dest_ssl: Optional[ssl.SSLContext] = None, 70 | timeout: Optional[float] = None, 71 | **kwargs: Any, 72 | ) -> AsyncioSocketStream: 73 | if timeout is None: 74 | timeout = DEFAULT_TIMEOUT 75 | 76 | local_addr = kwargs.get('local_addr') 77 | try: 78 | async with async_timeout.timeout(timeout): 79 | return await self._connect( 80 | dest_host=dest_host, 81 | dest_port=dest_port, 82 | dest_ssl=dest_ssl, 83 | local_addr=local_addr, 84 | ) 85 | except asyncio.TimeoutError as e: 86 | raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e 87 | 88 | async def _connect( 89 | self, 90 | dest_host: str, 91 | dest_port: int, 92 | dest_ssl: Optional[ssl.SSLContext] = None, 93 | local_addr: Optional[Tuple[str, int]] = None, 94 | ) -> AsyncioSocketStream: 95 | if self._forward is None: 96 | try: 97 | stream = await connect_tcp( 98 | host=self._proxy_host, 99 | port=self._proxy_port, 100 | loop=self._loop, 101 | local_addr=local_addr, 102 | ) 103 | except OSError as e: 104 | raise ProxyConnectionError( 105 | e.errno, 106 | "Couldn't connect to proxy" 107 | f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]", 108 | ) from e 109 | else: 110 | stream = await self._forward.connect( 111 | dest_host=self._proxy_host, 112 | dest_port=self._proxy_port, 113 | ) 114 | 115 | try: 116 | if self._proxy_ssl is not None: 117 | stream = await stream.start_tls( 118 | hostname=self._proxy_host, 119 | ssl_context=self._proxy_ssl, 120 | ) 121 | 122 | connector = create_connector( 123 | proxy_type=self._proxy_type, 124 | username=self._username, 125 | password=self._password, 126 | rdns=self._rdns, 127 | resolver=self._resolver, 128 | ) 129 | 130 | await connector.connect( 131 | stream=stream, 132 | host=dest_host, 133 | port=dest_port, 134 | ) 135 | 136 | if dest_ssl is not None: 137 | stream = await stream.start_tls( 138 | hostname=dest_host, 139 | ssl_context=dest_ssl, 140 | ) 141 | except ReplyError as e: 142 | await stream.close() 143 | raise ProxyError(e, error_code=e.error_code) 144 | except (asyncio.CancelledError, Exception): 145 | await stream.close() 146 | raise 147 | 148 | return stream 149 | 150 | @classmethod 151 | def create(cls, *args, **kwargs): # for backward compatibility 152 | return cls(*args, **kwargs) 153 | 154 | @classmethod 155 | def from_url(cls, url: str, **kwargs) -> 'AsyncioProxy': 156 | url_args = parse_proxy_url(url) 157 | return cls(*url_args, **kwargs) 158 | -------------------------------------------------------------------------------- /tests/test_proxy_async_aio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | 4 | import pytest # noqa 5 | from yarl import URL # noqa 6 | 7 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 8 | from python_socks.async_ import ProxyChain 9 | from python_socks.async_.asyncio import Proxy 10 | from python_socks.async_.asyncio._resolver import Resolver 11 | from tests.config import ( 12 | PROXY_HOST_IPV4, 13 | SOCKS5_PROXY_PORT, 14 | LOGIN, 15 | PASSWORD, 16 | SKIP_IPV6_TESTS, 17 | SOCKS5_IPV4_URL, 18 | SOCKS5_IPV4_URL_WO_AUTH, 19 | SOCKS5_IPV6_URL, 20 | SOCKS4_URL, 21 | HTTP_PROXY_URL, 22 | TEST_URL_IPV4, 23 | SOCKS5_IPV4_HOSTNAME_URL, 24 | TEST_URL_IPV4_HTTPS, 25 | ) 26 | 27 | 28 | async def make_request( 29 | proxy: Proxy, 30 | url: str, 31 | resolve_host=False, 32 | timeout=None, 33 | ssl_context=None, 34 | ): 35 | loop = asyncio.get_event_loop() 36 | 37 | url = URL(url) 38 | 39 | dest_host = url.host 40 | if resolve_host: 41 | resolver = Resolver(loop=loop) 42 | _, dest_host = await resolver.resolve(url.host) 43 | 44 | sock: socket.socket = await proxy.connect( 45 | dest_host=dest_host, dest_port=url.port, timeout=timeout 46 | ) 47 | 48 | if url.scheme == 'https': 49 | dest_ssl = ssl_context 50 | else: 51 | dest_ssl = None 52 | 53 | # noinspection PyTypeChecker 54 | reader, writer = await asyncio.open_connection( 55 | host=None, 56 | port=None, 57 | sock=sock, 58 | ssl=dest_ssl, 59 | server_hostname=url.host if dest_ssl else None, 60 | ) 61 | 62 | request = 'GET {rel_url} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n' 63 | request = request.format(rel_url=url.path_qs, host=url.host) 64 | request = request.encode('ascii') 65 | 66 | writer.write(request) 67 | 68 | status_line = await reader.readline() 69 | version, status_code, *reason = status_line.split() 70 | 71 | writer.transport.close() 72 | 73 | return int(status_code) 74 | 75 | 76 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 77 | @pytest.mark.parametrize('rdns', (True, False)) 78 | @pytest.mark.parametrize('resolve_host', (True, False)) 79 | @pytest.mark.asyncio 80 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 81 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 82 | status_code = await make_request( 83 | proxy=proxy, 84 | url=url, 85 | resolve_host=resolve_host, 86 | ssl_context=target_ssl_context, 87 | ) 88 | assert status_code == 200 89 | 90 | 91 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 92 | @pytest.mark.asyncio 93 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 94 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 95 | status_code = await make_request( 96 | proxy=proxy, 97 | url=url, 98 | ssl_context=target_ssl_context, 99 | ) 100 | assert status_code == 200 101 | 102 | 103 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 104 | @pytest.mark.parametrize('rdns', (None, True, False)) 105 | @pytest.mark.asyncio 106 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 107 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 108 | status_code = await make_request( 109 | proxy=proxy, 110 | url=url, 111 | ssl_context=target_ssl_context, 112 | ) 113 | assert status_code == 200 114 | 115 | 116 | @pytest.mark.asyncio 117 | async def test_socks5_proxy_with_invalid_credentials(): 118 | proxy = Proxy.create( 119 | proxy_type=ProxyType.SOCKS5, 120 | host=PROXY_HOST_IPV4, 121 | port=SOCKS5_PROXY_PORT, 122 | username=LOGIN, 123 | password=PASSWORD + 'aaa', 124 | ) 125 | with pytest.raises(ProxyError): 126 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 127 | 128 | 129 | @pytest.mark.asyncio 130 | async def test_socks5_proxy_with_connect_timeout(): 131 | proxy = Proxy.create( 132 | proxy_type=ProxyType.SOCKS5, 133 | host=PROXY_HOST_IPV4, 134 | port=SOCKS5_PROXY_PORT, 135 | username=LOGIN, 136 | password=PASSWORD, 137 | ) 138 | with pytest.raises(ProxyTimeoutError): 139 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 140 | 141 | 142 | @pytest.mark.asyncio 143 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 144 | proxy = Proxy.create( 145 | proxy_type=ProxyType.SOCKS5, 146 | host=PROXY_HOST_IPV4, 147 | port=unused_tcp_port, 148 | username=LOGIN, 149 | password=PASSWORD, 150 | ) 151 | with pytest.raises(ProxyConnectionError): 152 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 153 | 154 | 155 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 156 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 157 | @pytest.mark.asyncio 158 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 159 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 160 | status_code = await make_request( 161 | proxy=proxy, 162 | url=url, 163 | ssl_context=target_ssl_context, 164 | ) 165 | assert status_code == 200 166 | 167 | 168 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 169 | @pytest.mark.parametrize('rdns', (None, True, False)) 170 | @pytest.mark.parametrize('resolve_host', (True, False)) 171 | @pytest.mark.asyncio 172 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 173 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 174 | status_code = await make_request( 175 | proxy=proxy, 176 | url=url, 177 | resolve_host=resolve_host, 178 | ssl_context=target_ssl_context, 179 | ) 180 | assert status_code == 200 181 | 182 | 183 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 184 | @pytest.mark.asyncio 185 | async def test_http_proxy(url, target_ssl_context): 186 | proxy = Proxy.from_url(HTTP_PROXY_URL) 187 | status_code = await make_request( 188 | proxy=proxy, 189 | url=url, 190 | ssl_context=target_ssl_context, 191 | ) 192 | assert status_code == 200 193 | 194 | 195 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 196 | @pytest.mark.asyncio 197 | async def test_proxy_chain(url, target_ssl_context): 198 | proxy = ProxyChain( 199 | [ 200 | Proxy.from_url(SOCKS5_IPV4_URL), 201 | Proxy.from_url(SOCKS4_URL), 202 | Proxy.from_url(HTTP_PROXY_URL), 203 | ] 204 | ) 205 | # noinspection PyTypeChecker 206 | status_code = await make_request( 207 | proxy=proxy, 208 | url=url, 209 | ssl_context=target_ssl_context, 210 | ) 211 | assert status_code == 200 212 | -------------------------------------------------------------------------------- /python_socks/sync/v2/_ssl_transport.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copied from urllib3.util.ssltransport 3 | """ 4 | import io 5 | import socket 6 | import ssl 7 | 8 | 9 | SSL_BLOCKSIZE = 16384 10 | 11 | 12 | class SSLTransport: 13 | """ 14 | The SSLTransport wraps an existing socket and establishes an SSL connection. 15 | 16 | Contrary to Python's implementation of SSLSocket, it allows you to chain 17 | multiple TLS connections together. It's particularly useful if you need to 18 | implement TLS within TLS. 19 | 20 | The class supports most of the socket API operations. 21 | """ 22 | 23 | def __init__( 24 | self, socket, ssl_context, server_hostname=None, suppress_ragged_eofs=True 25 | ): 26 | """ 27 | Create an SSLTransport around socket using the provided ssl_context. 28 | """ 29 | self.incoming = ssl.MemoryBIO() 30 | self.outgoing = ssl.MemoryBIO() 31 | 32 | self.suppress_ragged_eofs = suppress_ragged_eofs 33 | self.socket = socket 34 | 35 | self.sslobj = ssl_context.wrap_bio( 36 | self.incoming, self.outgoing, server_hostname=server_hostname 37 | ) 38 | 39 | # Perform initial handshake. 40 | self._ssl_io_loop(self.sslobj.do_handshake) 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, *_): 46 | self.close() 47 | 48 | def fileno(self): 49 | return self.socket.fileno() 50 | 51 | def read(self, len=1024, buffer=None): 52 | return self._wrap_ssl_read(len, buffer) 53 | 54 | def recv(self, len=1024, flags=0): 55 | if flags != 0: 56 | raise ValueError("non-zero flags not allowed in calls to recv") 57 | return self._wrap_ssl_read(len) 58 | 59 | def recv_into(self, buffer, nbytes=None, flags=0): 60 | if flags != 0: 61 | raise ValueError("non-zero flags not allowed in calls to recv_into") 62 | if buffer and (nbytes is None): 63 | nbytes = len(buffer) 64 | elif nbytes is None: 65 | nbytes = 1024 66 | return self.read(nbytes, buffer) 67 | 68 | def sendall(self, data, flags=0): 69 | if flags != 0: 70 | raise ValueError("non-zero flags not allowed in calls to sendall") 71 | count = 0 72 | with memoryview(data) as view, view.cast("B") as byte_view: 73 | amount = len(byte_view) 74 | while count < amount: 75 | v = self.send(byte_view[count:]) 76 | count += v 77 | 78 | def send(self, data, flags=0): 79 | if flags != 0: 80 | raise ValueError("non-zero flags not allowed in calls to send") 81 | response = self._ssl_io_loop(self.sslobj.write, data) 82 | return response 83 | 84 | def makefile( 85 | self, mode="r", buffering=None, encoding=None, errors=None, newline=None 86 | ): 87 | """ 88 | Python's httpclient uses makefile and buffered io when reading HTTP 89 | messages and we need to support it. 90 | 91 | This is unfortunately a copy and paste of socket.py makefile with small 92 | changes to point to the socket directly. 93 | """ 94 | if not set(mode) <= {"r", "w", "b"}: 95 | raise ValueError("invalid mode %r (only r, w, b allowed)" % (mode,)) 96 | 97 | writing = "w" in mode 98 | reading = "r" in mode or not writing 99 | assert reading or writing 100 | binary = "b" in mode 101 | rawmode = "" 102 | if reading: 103 | rawmode += "r" 104 | if writing: 105 | rawmode += "w" 106 | raw = socket.SocketIO(self, rawmode) 107 | self.socket._io_refs += 1 108 | if buffering is None: 109 | buffering = -1 110 | if buffering < 0: 111 | buffering = io.DEFAULT_BUFFER_SIZE 112 | if buffering == 0: 113 | if not binary: 114 | raise ValueError("unbuffered streams must be binary") 115 | return raw 116 | if reading and writing: 117 | buffer = io.BufferedRWPair(raw, raw, buffering) 118 | elif reading: 119 | buffer = io.BufferedReader(raw, buffering) 120 | else: 121 | assert writing 122 | buffer = io.BufferedWriter(raw, buffering) 123 | if binary: 124 | return buffer 125 | text = io.TextIOWrapper(buffer, encoding, errors, newline) 126 | text.mode = mode 127 | return text 128 | 129 | def unwrap(self): 130 | self._ssl_io_loop(self.sslobj.unwrap) 131 | 132 | def close(self): 133 | self.socket.close() 134 | 135 | def getpeercert(self, binary_form=False): 136 | return self.sslobj.getpeercert(binary_form) 137 | 138 | def version(self): 139 | return self.sslobj.version() 140 | 141 | def cipher(self): 142 | return self.sslobj.cipher() 143 | 144 | def selected_alpn_protocol(self): 145 | return self.sslobj.selected_alpn_protocol() 146 | 147 | def selected_npn_protocol(self): 148 | return self.sslobj.selected_npn_protocol() 149 | 150 | def shared_ciphers(self): 151 | return self.sslobj.shared_ciphers() 152 | 153 | def compression(self): 154 | return self.sslobj.compression() 155 | 156 | def settimeout(self, value): 157 | self.socket.settimeout(value) 158 | 159 | def gettimeout(self): 160 | return self.socket.gettimeout() 161 | 162 | def _decref_socketios(self): 163 | self.socket._decref_socketios() 164 | 165 | def _wrap_ssl_read(self, len, buffer=None): 166 | try: 167 | return self._ssl_io_loop(self.sslobj.read, len, buffer) 168 | except ssl.SSLError as e: 169 | if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: 170 | return 0 # eof, return 0. 171 | else: 172 | raise 173 | 174 | def _ssl_io_loop(self, func, *args): 175 | """Performs an I/O loop between incoming/outgoing and the socket.""" 176 | should_loop = True 177 | ret = None 178 | 179 | while should_loop: 180 | errno = None 181 | try: 182 | ret = func(*args) 183 | except ssl.SSLError as e: 184 | if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): 185 | # WANT_READ, and WANT_WRITE are expected, others are not. 186 | raise e 187 | errno = e.errno 188 | 189 | buf = self.outgoing.read() 190 | self.socket.sendall(buf) 191 | 192 | if errno is None: 193 | should_loop = False 194 | elif errno == ssl.SSL_ERROR_WANT_READ: 195 | buf = self.socket.recv(SSL_BLOCKSIZE) 196 | if buf: 197 | self.incoming.write(buf) 198 | else: 199 | self.incoming.write_eof() 200 | return ret 201 | -------------------------------------------------------------------------------- /tests/test_proxy_async_trio.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import pytest 4 | from yarl import URL 5 | 6 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 7 | from python_socks.async_ import ProxyChain 8 | from tests.config import ( 9 | PROXY_HOST_IPV4, 10 | SOCKS5_PROXY_PORT, 11 | LOGIN, 12 | PASSWORD, 13 | SKIP_IPV6_TESTS, 14 | SOCKS5_IPV4_URL, 15 | SOCKS5_IPV4_URL_WO_AUTH, 16 | SOCKS5_IPV6_URL, 17 | SOCKS4_URL, 18 | HTTP_PROXY_URL, 19 | TEST_URL_IPV4, 20 | SOCKS5_IPV4_HOSTNAME_URL, 21 | TEST_URL_IPV4_HTTPS, 22 | ) 23 | 24 | trio = pytest.importorskip('trio') 25 | from python_socks.async_.trio import Proxy # noqa: E402 26 | from python_socks.async_.trio._resolver import Resolver # noqa: E402 27 | 28 | 29 | async def make_request( 30 | proxy: Proxy, 31 | url: str, 32 | resolve_host=False, 33 | timeout=None, 34 | ssl_context=None, 35 | ): 36 | url = URL(url) 37 | 38 | dest_host = url.host 39 | if resolve_host: 40 | resolver = Resolver() 41 | _, dest_host = await resolver.resolve(url.host) 42 | 43 | sock: socket.socket = await proxy.connect( 44 | dest_host=dest_host, dest_port=url.port, timeout=timeout 45 | ) 46 | 47 | if url.scheme == 'https': 48 | dest_ssl = ssl_context 49 | else: 50 | dest_ssl = None 51 | 52 | stream = trio.SocketStream(sock) 53 | 54 | if dest_ssl is not None: 55 | stream = trio.SSLStream(stream, dest_ssl, server_hostname=url.host) 56 | await stream.do_handshake() 57 | 58 | # fmt: off 59 | request = ( 60 | 'GET {rel_url} HTTP/1.1\r\n' 61 | 'Host: {host}\r\n' 62 | 'Connection: close\r\n\r\n' 63 | ) 64 | # fmt: on 65 | request = request.format(rel_url=url.path_qs, host=url.host) 66 | request = request.encode('ascii') 67 | 68 | await stream.send_all(request) 69 | 70 | response = await stream.receive_some(1024) 71 | 72 | status_line = response.split(b'\r\n', 1)[0] 73 | status_line = status_line.decode('utf-8', 'surrogateescape') 74 | version, status_code, *reason = status_line.split() 75 | 76 | return int(status_code) 77 | 78 | 79 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 80 | @pytest.mark.parametrize('rdns', (True, False)) 81 | @pytest.mark.parametrize('resolve_host', (True, False)) 82 | @pytest.mark.trio 83 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 84 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 85 | status_code = await make_request( 86 | proxy=proxy, 87 | url=url, 88 | resolve_host=resolve_host, 89 | ssl_context=target_ssl_context, 90 | ) 91 | assert status_code == 200 92 | 93 | 94 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 95 | @pytest.mark.trio 96 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 97 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 98 | status_code = await make_request( 99 | proxy=proxy, 100 | url=url, 101 | ssl_context=target_ssl_context, 102 | ) 103 | assert status_code == 200 104 | 105 | 106 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 107 | @pytest.mark.parametrize('rdns', (None, True, False)) 108 | @pytest.mark.trio 109 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 110 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 111 | status_code = await make_request( 112 | proxy=proxy, 113 | url=url, 114 | ssl_context=target_ssl_context, 115 | ) 116 | assert status_code == 200 117 | 118 | 119 | @pytest.mark.trio 120 | async def test_socks5_proxy_with_invalid_credentials(): 121 | proxy = Proxy.create( 122 | proxy_type=ProxyType.SOCKS5, 123 | host=PROXY_HOST_IPV4, 124 | port=SOCKS5_PROXY_PORT, 125 | username=LOGIN, 126 | password=PASSWORD + 'aaa', 127 | ) 128 | with pytest.raises(ProxyError): 129 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 130 | 131 | 132 | @pytest.mark.trio 133 | async def test_socks5_proxy_with_connect_timeout(): 134 | proxy = Proxy.create( 135 | proxy_type=ProxyType.SOCKS5, 136 | host=PROXY_HOST_IPV4, 137 | port=SOCKS5_PROXY_PORT, 138 | username=LOGIN, 139 | password=PASSWORD, 140 | ) 141 | with pytest.raises(ProxyTimeoutError): 142 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 143 | 144 | 145 | @pytest.mark.trio 146 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 147 | proxy = Proxy.create( 148 | proxy_type=ProxyType.SOCKS5, 149 | host=PROXY_HOST_IPV4, 150 | port=unused_tcp_port, 151 | username=LOGIN, 152 | password=PASSWORD, 153 | ) 154 | with pytest.raises(ProxyConnectionError): 155 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 156 | 157 | 158 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 159 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 160 | @pytest.mark.trio 161 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 162 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 163 | status_code = await make_request( 164 | proxy=proxy, 165 | url=url, 166 | ssl_context=target_ssl_context, 167 | ) 168 | assert status_code == 200 169 | 170 | 171 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 172 | @pytest.mark.parametrize('rdns', (None, True, False)) 173 | @pytest.mark.parametrize('resolve_host', (True, False)) 174 | @pytest.mark.trio 175 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 176 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 177 | status_code = await make_request( 178 | proxy=proxy, 179 | url=url, 180 | resolve_host=resolve_host, 181 | ssl_context=target_ssl_context, 182 | ) 183 | assert status_code == 200 184 | 185 | 186 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 187 | @pytest.mark.trio 188 | async def test_http_proxy(url, target_ssl_context): 189 | proxy = Proxy.from_url(HTTP_PROXY_URL) 190 | status_code = await make_request( 191 | proxy=proxy, 192 | url=url, 193 | ssl_context=target_ssl_context, 194 | ) 195 | assert status_code == 200 196 | 197 | 198 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 199 | @pytest.mark.trio 200 | async def test_proxy_chain(url, target_ssl_context): 201 | proxy = ProxyChain( 202 | [ 203 | Proxy.from_url(SOCKS5_IPV4_URL), 204 | Proxy.from_url(SOCKS4_URL), 205 | Proxy.from_url(HTTP_PROXY_URL), 206 | ] 207 | ) 208 | status_code = await make_request( 209 | proxy=proxy, # type: ignore 210 | url=url, 211 | ssl_context=target_ssl_context, 212 | ) 213 | assert status_code == 200 214 | -------------------------------------------------------------------------------- /tests/test_proxy_sync.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from unittest import mock 3 | 4 | import pytest 5 | from yarl import URL 6 | 7 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 8 | from python_socks.sync import Proxy 9 | from python_socks.sync import ProxyChain 10 | from python_socks.sync._proxy import SyncProxy # noqa 11 | from python_socks.sync._resolver import SyncResolver # noqa 12 | from tests.config import ( 13 | PROXY_HOST_IPV4, 14 | SOCKS5_PROXY_PORT, 15 | LOGIN, 16 | PASSWORD, 17 | SKIP_IPV6_TESTS, 18 | SOCKS5_IPV4_URL, 19 | SOCKS5_IPV4_URL_WO_AUTH, 20 | SOCKS5_IPV6_URL, 21 | SOCKS4_URL, 22 | HTTP_PROXY_URL, 23 | HTTP_PROXY_PORT, 24 | TEST_URL_IPV4, 25 | TEST_URL_IPv6, 26 | SOCKS5_IPV4_HOSTNAME_URL, 27 | TEST_URL_IPV4_HTTPS, 28 | ) 29 | from tests.mocks import getaddrinfo_sync_mock 30 | 31 | 32 | def read_status_code(sock: socket.socket) -> int: 33 | data = sock.recv(1024) 34 | status_line = data.split(b'\r\n', 1)[0] 35 | status_line = status_line.decode('utf-8', 'surrogateescape') 36 | version, status_code, *reason = status_line.split() 37 | return int(status_code) 38 | 39 | 40 | def make_request( 41 | proxy: SyncProxy, 42 | url: str, 43 | resolve_host=False, 44 | timeout=None, 45 | ssl_context=None, 46 | ): 47 | with mock.patch('socket.getaddrinfo', new=getaddrinfo_sync_mock()): 48 | url = URL(url) 49 | 50 | dest_host = url.host 51 | if resolve_host: 52 | resolver = SyncResolver() 53 | _, dest_host = resolver.resolve(url.host) 54 | 55 | sock: socket.socket = proxy.connect( 56 | dest_host=dest_host, dest_port=url.port, timeout=timeout 57 | ) 58 | 59 | if url.scheme == 'https': 60 | assert ssl_context is not None 61 | sock = ssl_context.wrap_socket(sock=sock, server_hostname=url.host) 62 | 63 | request = 'GET {rel_url} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\n\r\n' 64 | request = request.format(rel_url=url.path_qs, host=url.host) 65 | request = request.encode('ascii') 66 | sock.sendall(request) 67 | 68 | status_code = read_status_code(sock) 69 | sock.close() 70 | return status_code 71 | 72 | 73 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 74 | @pytest.mark.parametrize('rdns', (True, False)) 75 | @pytest.mark.parametrize('resolve_host', (True, False)) 76 | def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 77 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 78 | status_code = make_request( 79 | proxy=proxy, 80 | url=url, 81 | resolve_host=resolve_host, 82 | ssl_context=target_ssl_context, 83 | ) 84 | assert status_code == 200 85 | 86 | 87 | def test_socks5_proxy_hostname_ipv4(): 88 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 89 | status_code = make_request( 90 | proxy=proxy, 91 | url=TEST_URL_IPV4, 92 | ) 93 | assert status_code == 200 94 | 95 | 96 | @pytest.mark.parametrize('rdns', (None, True, False)) 97 | def test_socks5_proxy_ipv4_with_auth_none(rdns): 98 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 99 | status_code = make_request(proxy=proxy, url=TEST_URL_IPV4) 100 | assert status_code == 200 101 | 102 | 103 | def test_socks5_proxy_with_invalid_credentials(): 104 | proxy = Proxy.create( 105 | proxy_type=ProxyType.SOCKS5, 106 | host=PROXY_HOST_IPV4, 107 | port=SOCKS5_PROXY_PORT, 108 | username=LOGIN, 109 | password=PASSWORD + 'aaa', 110 | ) 111 | with pytest.raises(ProxyError): 112 | make_request(proxy=proxy, url=TEST_URL_IPV4) 113 | 114 | 115 | def test_socks5_proxy_with_connect_timeout(): 116 | proxy = Proxy.create( 117 | proxy_type=ProxyType.SOCKS5, 118 | host=PROXY_HOST_IPV4, 119 | port=SOCKS5_PROXY_PORT, 120 | username=LOGIN, 121 | password=PASSWORD, 122 | ) 123 | with pytest.raises(ProxyTimeoutError): 124 | make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.001) 125 | 126 | 127 | def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 128 | proxy = Proxy.create( 129 | proxy_type=ProxyType.SOCKS5, 130 | host=PROXY_HOST_IPV4, 131 | port=unused_tcp_port, 132 | username=LOGIN, 133 | password=PASSWORD, 134 | ) 135 | with pytest.raises(ProxyConnectionError): 136 | make_request(proxy=proxy, url=TEST_URL_IPV4) 137 | 138 | 139 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 140 | def test_socks5_proxy_ipv6(): 141 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 142 | status_code = make_request(proxy=proxy, url=TEST_URL_IPV4) 143 | assert status_code == 200 144 | 145 | 146 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 147 | @pytest.mark.parametrize('rdns', (True, False)) 148 | def test_socks5_proxy_hostname_ipv6(rdns): 149 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 150 | status_code = make_request(proxy=proxy, url=TEST_URL_IPv6) 151 | assert status_code == 200 152 | 153 | 154 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 155 | @pytest.mark.parametrize('rdns', (None, True, False)) 156 | @pytest.mark.parametrize('resolve_host', (True, False)) 157 | def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 158 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 159 | status_code = make_request( 160 | proxy=proxy, 161 | url=url, 162 | resolve_host=resolve_host, 163 | ssl_context=target_ssl_context, 164 | ) 165 | assert status_code == 200 166 | 167 | 168 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 169 | def test_http_proxy(url, target_ssl_context): 170 | proxy = Proxy.from_url(HTTP_PROXY_URL) 171 | status_code = make_request( 172 | proxy=proxy, 173 | url=url, 174 | ssl_context=target_ssl_context, 175 | ) 176 | assert status_code == 200 177 | 178 | 179 | def test_http_proxy_with_invalid_credentials(): 180 | proxy = Proxy.create( 181 | proxy_type=ProxyType.HTTP, 182 | host=PROXY_HOST_IPV4, 183 | port=HTTP_PROXY_PORT, 184 | username=LOGIN, 185 | password=PASSWORD + 'aaa', 186 | ) 187 | with pytest.raises(ProxyError): 188 | make_request(proxy=proxy, url=TEST_URL_IPV4) 189 | 190 | 191 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 192 | def test_proxy_chain(url, target_ssl_context): 193 | proxy = ProxyChain( 194 | [ 195 | Proxy.from_url(SOCKS5_IPV4_URL), 196 | Proxy.from_url(SOCKS4_URL), 197 | Proxy.from_url(HTTP_PROXY_URL), 198 | ] 199 | ) 200 | status_code = make_request( 201 | proxy=proxy, # type: ignore 202 | url=url, 203 | ssl_context=target_ssl_context, 204 | ) 205 | assert status_code == 200 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## python-socks 2 | 3 | [![CI](https://github.com/romis2012/python-socks/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/python-socks/actions/workflows/ci.yml) 4 | [![Coverage Status](https://codecov.io/gh/romis2012/python-socks/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/python-socks) 5 | [![PyPI version](https://badge.fury.io/py/python-socks.svg)](https://pypi.python.org/pypi/python-socks) 6 | [![versions](https://img.shields.io/pypi/pyversions/python-socks.svg)](https://github.com/romis2012/python-socks) 7 | 10 | 11 | The `python-socks` package provides a core proxy client functionality for Python. 12 | Supports `SOCKS4(a)`, `SOCKS5(h)`, `HTTP CONNECT` proxy and provides sync and async (asyncio, trio, curio, anyio) APIs. 13 | You probably don't need to use `python-socks` directly. 14 | It is used internally by 15 | [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages. 16 | 17 | ## Requirements 18 | - Python >= 3.8 19 | - async-timeout >= 4.0 (optional) 20 | - trio >= 0.24 (optional) 21 | - curio >= 1.4 (optional) 22 | - anyio >= 3.3.4 (optional) 23 | 24 | ## Installation 25 | 26 | only sync proxy support: 27 | ``` 28 | pip install python-socks 29 | ``` 30 | 31 | to include optional asyncio support: 32 | ``` 33 | pip install python-socks[asyncio] 34 | ``` 35 | 36 | to include optional trio support: 37 | ``` 38 | pip install python-socks[trio] 39 | ``` 40 | 41 | to include optional curio support: 42 | ``` 43 | pip install python-socks[curio] 44 | ``` 45 | 46 | to include optional anyio support: 47 | ``` 48 | pip install python-socks[anyio] 49 | ``` 50 | 51 | ## Simple usage 52 | We are making secure HTTP GET request via SOCKS5 proxy 53 | 54 | #### Sync 55 | ```python 56 | import ssl 57 | from python_socks.sync import Proxy 58 | 59 | proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080') 60 | 61 | # `connect` returns standard Python socket in blocking mode 62 | sock = proxy.connect(dest_host='check-host.net', dest_port=443) 63 | 64 | sock = ssl.create_default_context().wrap_socket( 65 | sock=sock, 66 | server_hostname='check-host.net' 67 | ) 68 | 69 | request = ( 70 | b'GET /ip HTTP/1.1\r\n' 71 | b'Host: check-host.net\r\n' 72 | b'Connection: close\r\n\r\n' 73 | ) 74 | sock.sendall(request) 75 | response = sock.recv(4096) 76 | print(response) 77 | ``` 78 | 79 | #### Async (asyncio) 80 | ```python 81 | import ssl 82 | import asyncio 83 | from python_socks.async_.asyncio import Proxy 84 | 85 | proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080') 86 | 87 | # `connect` returns standard Python socket in non-blocking mode 88 | # so we can pass it to asyncio.open_connection(...) 89 | sock = await proxy.connect(dest_host='check-host.net', dest_port=443) 90 | 91 | reader, writer = await asyncio.open_connection( 92 | host=None, 93 | port=None, 94 | sock=sock, 95 | ssl=ssl.create_default_context(), 96 | server_hostname='check-host.net', 97 | ) 98 | 99 | request = ( 100 | b'GET /ip HTTP/1.1\r\n' 101 | b'Host: check-host.net\r\n' 102 | b'Connection: close\r\n\r\n' 103 | ) 104 | 105 | writer.write(request) 106 | response = await reader.read(-1) 107 | print(response) 108 | ``` 109 | 110 | #### Async (trio) 111 | ```python 112 | import ssl 113 | import trio 114 | from python_socks.async_.trio import Proxy 115 | 116 | proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080') 117 | 118 | # `connect` returns trio socket 119 | # so we can pass it to trio.SocketStream 120 | sock = await proxy.connect(dest_host='check-host.net', dest_port=443) 121 | 122 | stream = trio.SocketStream(sock) 123 | 124 | stream = trio.SSLStream( 125 | stream, ssl.create_default_context(), 126 | server_hostname='check-host.net' 127 | ) 128 | await stream.do_handshake() 129 | 130 | request = ( 131 | b'GET /ip HTTP/1.1\r\n' 132 | b'Host: check-host.net\r\n' 133 | b'Connection: close\r\n\r\n' 134 | ) 135 | 136 | await stream.send_all(request) 137 | response = await stream.receive_some(4096) 138 | print(response) 139 | ``` 140 | 141 | #### Async (curio) 142 | ```python 143 | import curio.ssl as curiossl 144 | from python_socks.async_.curio import Proxy 145 | 146 | proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080') 147 | # `connect` returns curio.io.Socket 148 | sock = await proxy.connect( 149 | dest_host='check-host.net', 150 | dest_port=443 151 | ) 152 | 153 | request = ( 154 | b'GET /ip HTTP/1.1\r\n' 155 | b'Host: check-host.net\r\n' 156 | b'Connection: close\r\n\r\n' 157 | ) 158 | 159 | ssl_context = curiossl.create_default_context() 160 | sock = await ssl_context.wrap_socket( 161 | sock, do_handshake_on_connect=False, server_hostname='check-host.net' 162 | ) 163 | 164 | await sock.do_handshake() 165 | 166 | stream = sock.as_stream() 167 | 168 | await stream.write(request) 169 | response = await stream.read(1024) 170 | print(response) 171 | ``` 172 | 173 | #### Async (anyio) 174 | ```python 175 | import ssl 176 | from python_socks.async_.anyio import Proxy 177 | 178 | proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080') 179 | 180 | # `connect` returns AnyioSocketStream 181 | stream = await proxy.connect( 182 | dest_host='check-host.net', 183 | dest_port=443, 184 | dest_ssl=ssl.create_default_context(), 185 | ) 186 | 187 | request = ( 188 | b'GET /ip HTTP/1.1\r\n' 189 | b'Host: check-host.net\r\n' 190 | b'Connection: close\r\n\r\n' 191 | ) 192 | 193 | await stream.write_all(request) 194 | response = await stream.read() 195 | print(response) 196 | ``` 197 | 198 | ## More complex example 199 | 200 | #### A urllib3 PoolManager that routes connections via the proxy 201 | 202 | ```python 203 | from urllib3 import PoolManager, HTTPConnectionPool, HTTPSConnectionPool 204 | from urllib3.connection import HTTPConnection, HTTPSConnection 205 | from python_socks.sync import Proxy 206 | 207 | 208 | class ProxyHTTPConnection(HTTPConnection): 209 | def __init__(self, *args, **kwargs): 210 | socks_options = kwargs.pop('_socks_options') 211 | self._proxy_url = socks_options['proxy_url'] 212 | super().__init__(*args, **kwargs) 213 | 214 | def _new_conn(self): 215 | proxy = Proxy.from_url(self._proxy_url) 216 | return proxy.connect( 217 | dest_host=self.host, 218 | dest_port=self.port, 219 | timeout=self.timeout 220 | ) 221 | 222 | 223 | class ProxyHTTPSConnection(ProxyHTTPConnection, HTTPSConnection): 224 | pass 225 | 226 | 227 | class ProxyHTTPConnectionPool(HTTPConnectionPool): 228 | ConnectionCls = ProxyHTTPConnection 229 | 230 | 231 | class ProxyHTTPSConnectionPool(HTTPSConnectionPool): 232 | ConnectionCls = ProxyHTTPSConnection 233 | 234 | 235 | class ProxyPoolManager(PoolManager): 236 | def __init__(self, proxy_url, timeout=5, num_pools=10, headers=None, 237 | **connection_pool_kw): 238 | 239 | connection_pool_kw['_socks_options'] = {'proxy_url': proxy_url} 240 | connection_pool_kw['timeout'] = timeout 241 | 242 | super().__init__(num_pools, headers, **connection_pool_kw) 243 | 244 | self.pool_classes_by_scheme = { 245 | 'http': ProxyHTTPConnectionPool, 246 | 'https': ProxyHTTPSConnectionPool, 247 | } 248 | 249 | 250 | ### and how to use it 251 | manager = ProxyPoolManager('socks5://user:password@127.0.0.1:1080') 252 | response = manager.request('GET', 'https://check-host.net/ip') 253 | print(response.data) 254 | ``` 255 | -------------------------------------------------------------------------------- /tests/test_proxy_async_aio_v2.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | from yarl import URL 7 | 8 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 9 | from python_socks.async_.asyncio._resolver import Resolver 10 | from python_socks.async_.asyncio.v2 import Proxy 11 | from python_socks.async_.asyncio.v2 import ProxyChain 12 | from python_socks.async_.asyncio.v2._proxy import AsyncioProxy 13 | from tests.config import ( 14 | PROXY_HOST_IPV4, 15 | SOCKS5_PROXY_PORT, 16 | LOGIN, 17 | PASSWORD, 18 | SKIP_IPV6_TESTS, 19 | SOCKS5_IPV4_URL, 20 | SOCKS5_IPV4_URL_WO_AUTH, 21 | SOCKS5_IPV6_URL, 22 | SOCKS4_URL, 23 | HTTP_PROXY_URL, 24 | TEST_URL_IPV4, 25 | SOCKS5_IPV4_HOSTNAME_URL, 26 | TEST_URL_IPV4_HTTPS, TEST_URL_IPv6, 27 | ) 28 | from tests.mocks import getaddrinfo_async_mock 29 | 30 | 31 | async def make_request( 32 | proxy: AsyncioProxy, 33 | url: str, 34 | resolve_host=False, 35 | timeout=None, 36 | ssl_context=None, 37 | ): 38 | loop = asyncio.get_event_loop() 39 | with patch.object( 40 | loop, 41 | attribute='getaddrinfo', 42 | new=getaddrinfo_async_mock(loop.getaddrinfo), 43 | ): 44 | url = URL(url) 45 | 46 | dest_host = url.host 47 | if resolve_host: 48 | resolver = Resolver(loop=loop) 49 | _, dest_host = await resolver.resolve(url.host) 50 | 51 | if url.scheme == 'https': 52 | dest_ssl = ssl_context 53 | else: 54 | dest_ssl = None 55 | 56 | stream = await proxy.connect( 57 | dest_host=dest_host, 58 | dest_port=url.port, 59 | dest_ssl=dest_ssl, 60 | timeout=timeout, 61 | ) 62 | 63 | # fmt: off 64 | request = ( 65 | 'GET {rel_url} HTTP/1.1\r\n' 66 | 'Host: {host}\r\n' 67 | 'Connection: close\r\n\r\n' 68 | ) 69 | # fmt: on 70 | 71 | request = request.format(rel_url=url.path_qs, host=url.host) 72 | request = request.encode('ascii') 73 | 74 | await stream.write_all(request) 75 | 76 | response = await stream.read(1024) 77 | 78 | status_line = response.split(b'\r\n', 1)[0] 79 | version, status_code, *reason = status_line.split() 80 | 81 | await stream.close() 82 | 83 | return int(status_code) 84 | 85 | 86 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 87 | @pytest.mark.parametrize('rdns', (True, False)) 88 | @pytest.mark.parametrize('resolve_host', (True, False)) 89 | @pytest.mark.asyncio 90 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 91 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 92 | status_code = await make_request( 93 | proxy=proxy, 94 | url=url, 95 | resolve_host=resolve_host, 96 | ssl_context=target_ssl_context, 97 | ) 98 | assert status_code == 200 99 | 100 | 101 | @pytest.mark.skipif(sys.version_info < (3, 7), reason="Buggy asyncio...") 102 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 103 | @pytest.mark.asyncio 104 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 105 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 106 | status_code = await make_request(proxy=proxy, url=url, ssl_context=target_ssl_context) 107 | assert status_code == 200 108 | 109 | 110 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 111 | @pytest.mark.parametrize('rdns', (None, True, False)) 112 | @pytest.mark.asyncio 113 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 114 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 115 | status_code = await make_request( 116 | proxy=proxy, 117 | url=url, 118 | ssl_context=target_ssl_context, 119 | ) 120 | assert status_code == 200 121 | 122 | 123 | @pytest.mark.asyncio 124 | async def test_socks5_proxy_with_invalid_credentials(): 125 | proxy = Proxy.create( 126 | proxy_type=ProxyType.SOCKS5, 127 | host=PROXY_HOST_IPV4, 128 | port=SOCKS5_PROXY_PORT, 129 | username=LOGIN, 130 | password=PASSWORD + 'aaa', 131 | ) 132 | with pytest.raises(ProxyError): 133 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 134 | 135 | 136 | @pytest.mark.asyncio 137 | async def test_socks5_proxy_with_connect_timeout(): 138 | proxy = Proxy.create( 139 | proxy_type=ProxyType.SOCKS5, 140 | host=PROXY_HOST_IPV4, 141 | port=SOCKS5_PROXY_PORT, 142 | username=LOGIN, 143 | password=PASSWORD, 144 | ) 145 | with pytest.raises(ProxyTimeoutError): 146 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 147 | 148 | 149 | @pytest.mark.asyncio 150 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 151 | proxy = Proxy.create( 152 | proxy_type=ProxyType.SOCKS5, 153 | host=PROXY_HOST_IPV4, 154 | port=unused_tcp_port, 155 | username=LOGIN, 156 | password=PASSWORD, 157 | ) 158 | with pytest.raises(ProxyConnectionError): 159 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 160 | 161 | 162 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 163 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 164 | @pytest.mark.asyncio 165 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 166 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 167 | status_code = await make_request( 168 | proxy=proxy, 169 | url=url, 170 | ssl_context=target_ssl_context, 171 | ) 172 | assert status_code == 200 173 | 174 | 175 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 176 | @pytest.mark.parametrize('rdns', (True, False)) 177 | @pytest.mark.asyncio 178 | async def test_socks5_proxy_hostname_ipv6(rdns): 179 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 180 | status_code = await make_request(proxy=proxy, url=TEST_URL_IPv6) 181 | assert status_code == 200 182 | 183 | 184 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 185 | @pytest.mark.parametrize('rdns', (None, True, False)) 186 | @pytest.mark.parametrize('resolve_host', (True, False)) 187 | @pytest.mark.asyncio 188 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 189 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 190 | status_code = await make_request( 191 | proxy=proxy, 192 | url=url, 193 | resolve_host=resolve_host, 194 | ssl_context=target_ssl_context, 195 | ) 196 | assert status_code == 200 197 | 198 | 199 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 200 | @pytest.mark.asyncio 201 | async def test_http_proxy(url, target_ssl_context): 202 | proxy = Proxy.from_url(HTTP_PROXY_URL) 203 | status_code = await make_request( 204 | proxy=proxy, 205 | url=url, 206 | ssl_context=target_ssl_context, 207 | ) 208 | assert status_code == 200 209 | 210 | 211 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 212 | @pytest.mark.asyncio 213 | async def test_proxy_chain(url, target_ssl_context): 214 | proxy = ProxyChain( 215 | [ 216 | Proxy.from_url(SOCKS5_IPV4_URL), 217 | Proxy.from_url(SOCKS4_URL), 218 | Proxy.from_url(HTTP_PROXY_URL), 219 | ] 220 | ) 221 | status_code = await make_request( 222 | proxy=proxy, # type: ignore 223 | url=url, 224 | ssl_context=target_ssl_context, 225 | ) 226 | assert status_code == 200 227 | -------------------------------------------------------------------------------- /tests/test_proxy_async_anyio.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from yarl import URL 5 | 6 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 7 | from tests.config import ( 8 | PROXY_HOST_IPV4, 9 | SOCKS5_PROXY_PORT, 10 | LOGIN, 11 | PASSWORD, 12 | SKIP_IPV6_TESTS, 13 | SOCKS5_IPV4_URL, 14 | SOCKS5_IPV4_URL_WO_AUTH, 15 | SOCKS5_IPV6_URL, 16 | SOCKS4_URL, 17 | HTTP_PROXY_URL, 18 | TEST_URL_IPV4, 19 | SOCKS5_IPV4_HOSTNAME_URL, 20 | TEST_URL_IPV4_HTTPS, 21 | HTTPS_PROXY_URL, 22 | ) 23 | from tests.mocks import getaddrinfo_async_mock 24 | 25 | anyio = pytest.importorskip('anyio') 26 | 27 | from python_socks.async_.anyio._resolver import Resolver # noqa: E402 28 | from python_socks.async_.anyio import Proxy # noqa: E402 29 | from python_socks.async_.anyio import ProxyChain # noqa: E402 30 | from python_socks.async_.anyio._proxy import AnyioProxy # noqa: E402 31 | 32 | 33 | async def make_request( 34 | proxy: AnyioProxy, 35 | url: str, 36 | resolve_host=False, 37 | timeout=None, 38 | ssl_context=None, 39 | ): 40 | # import anyio 41 | with patch( 42 | 'anyio._core._sockets.getaddrinfo', 43 | new=getaddrinfo_async_mock(anyio.getaddrinfo), 44 | ): 45 | url = URL(url) 46 | 47 | dest_host = url.host 48 | if resolve_host: 49 | resolver = Resolver() 50 | _, dest_host = await resolver.resolve(url.host) 51 | 52 | dest_ssl = None 53 | if url.scheme == 'https': 54 | dest_ssl = ssl_context 55 | 56 | stream = await proxy.connect( 57 | dest_host=dest_host, 58 | dest_port=url.port, 59 | dest_ssl=dest_ssl, 60 | timeout=timeout, 61 | ) 62 | 63 | # fmt: off 64 | request = ( 65 | 'GET {rel_url} HTTP/1.1\r\n' 66 | 'Host: {host}\r\n' 67 | 'Connection: close\r\n\r\n' 68 | ) 69 | # fmt: on 70 | 71 | request = request.format(rel_url=url.path_qs, host=url.host) 72 | request = request.encode('ascii') 73 | 74 | await stream.write_all(request) 75 | 76 | response = await stream.read(1024) 77 | 78 | status_line = response.split(b'\r\n', 1)[0] 79 | version, status_code, *reason = status_line.split() 80 | 81 | await stream.close() 82 | 83 | return int(status_code) 84 | 85 | 86 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 87 | @pytest.mark.parametrize('rdns', (True, False)) 88 | @pytest.mark.parametrize('resolve_host', (True, False)) 89 | @pytest.mark.anyio 90 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 91 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 92 | status_code = await make_request( 93 | proxy=proxy, 94 | url=url, 95 | resolve_host=resolve_host, 96 | ssl_context=target_ssl_context, 97 | ) 98 | assert status_code == 200 99 | 100 | 101 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 102 | @pytest.mark.anyio 103 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 104 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 105 | status_code = await make_request( 106 | proxy=proxy, 107 | url=url, 108 | ssl_context=target_ssl_context, 109 | ) 110 | assert status_code == 200 111 | 112 | 113 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 114 | @pytest.mark.parametrize('rdns', (None, True, False)) 115 | @pytest.mark.anyio 116 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 117 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 118 | status_code = await make_request( 119 | proxy=proxy, 120 | url=url, 121 | ssl_context=target_ssl_context, 122 | ) 123 | assert status_code == 200 124 | 125 | 126 | @pytest.mark.anyio 127 | async def test_socks5_proxy_with_invalid_credentials(): 128 | proxy = Proxy.create( 129 | proxy_type=ProxyType.SOCKS5, 130 | host=PROXY_HOST_IPV4, 131 | port=SOCKS5_PROXY_PORT, 132 | username=LOGIN, 133 | password=PASSWORD + 'aaa', 134 | ) 135 | with pytest.raises(ProxyError): 136 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 137 | 138 | 139 | @pytest.mark.anyio 140 | async def test_socks5_proxy_with_connect_timeout(): 141 | proxy = Proxy.create( 142 | proxy_type=ProxyType.SOCKS5, 143 | host=PROXY_HOST_IPV4, 144 | port=SOCKS5_PROXY_PORT, 145 | username=LOGIN, 146 | password=PASSWORD, 147 | ) 148 | with pytest.raises(ProxyTimeoutError): 149 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 150 | 151 | 152 | @pytest.mark.anyio 153 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 154 | proxy = Proxy.create( 155 | proxy_type=ProxyType.SOCKS5, 156 | host=PROXY_HOST_IPV4, 157 | port=unused_tcp_port, 158 | username=LOGIN, 159 | password=PASSWORD, 160 | ) 161 | with pytest.raises(ProxyConnectionError): 162 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 163 | 164 | 165 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 166 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 167 | @pytest.mark.anyio 168 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 169 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 170 | status_code = await make_request( 171 | proxy=proxy, 172 | url=url, 173 | ssl_context=target_ssl_context, 174 | ) 175 | assert status_code == 200 176 | 177 | 178 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 179 | @pytest.mark.parametrize('rdns', (None, True, False)) 180 | @pytest.mark.parametrize('resolve_host', (True, False)) 181 | @pytest.mark.anyio 182 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 183 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 184 | status_code = await make_request( 185 | proxy=proxy, 186 | url=url, 187 | resolve_host=resolve_host, 188 | ssl_context=target_ssl_context, 189 | ) 190 | assert status_code == 200 191 | 192 | 193 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 194 | @pytest.mark.anyio 195 | async def test_http_proxy(url, target_ssl_context): 196 | proxy = Proxy.from_url(HTTP_PROXY_URL) 197 | status_code = await make_request( 198 | proxy=proxy, 199 | url=url, 200 | ssl_context=target_ssl_context, 201 | ) 202 | assert status_code == 200 203 | 204 | 205 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 206 | @pytest.mark.anyio 207 | async def test_secure_proxy(url, target_ssl_context, proxy_ssl_context): 208 | proxy = Proxy.from_url(HTTPS_PROXY_URL, proxy_ssl=proxy_ssl_context) 209 | status_code = await make_request( 210 | proxy=proxy, 211 | url=url, 212 | ssl_context=target_ssl_context, 213 | ) 214 | assert status_code == 200 215 | 216 | 217 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 218 | @pytest.mark.anyio 219 | async def test_proxy_chain(url, target_ssl_context): 220 | proxy = ProxyChain( 221 | [ 222 | Proxy.from_url(SOCKS5_IPV4_URL), 223 | Proxy.from_url(SOCKS4_URL), 224 | Proxy.from_url(HTTP_PROXY_URL), 225 | ] 226 | ) 227 | status_code = await make_request( 228 | proxy=proxy, # type: ignore 229 | url=url, 230 | ssl_context=target_ssl_context, 231 | ) 232 | assert status_code == 200 233 | -------------------------------------------------------------------------------- /tests/test_proxy_async_trio_v2.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from yarl import URL 5 | 6 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 7 | from tests.config import ( 8 | PROXY_HOST_IPV4, 9 | SOCKS5_PROXY_PORT, 10 | LOGIN, 11 | PASSWORD, 12 | SKIP_IPV6_TESTS, 13 | SOCKS5_IPV4_URL, 14 | SOCKS5_IPV4_URL_WO_AUTH, 15 | SOCKS5_IPV6_URL, 16 | SOCKS4_URL, 17 | HTTP_PROXY_URL, 18 | TEST_URL_IPV4, 19 | SOCKS5_IPV4_HOSTNAME_URL, 20 | TEST_URL_IPV4_HTTPS, 21 | HTTPS_PROXY_URL, 22 | ) 23 | from tests.mocks import getaddrinfo_async_mock 24 | 25 | trio = pytest.importorskip('trio') 26 | 27 | from python_socks.async_.trio._resolver import Resolver # noqa: E402 28 | from python_socks.async_.trio.v2 import Proxy # noqa: E402 29 | from python_socks.async_.trio.v2 import ProxyChain # noqa: E402 30 | from python_socks.async_.trio.v2._proxy import TrioProxy # noqa: E402 31 | 32 | 33 | async def make_request( 34 | proxy: TrioProxy, 35 | url: str, 36 | resolve_host=False, 37 | timeout=None, 38 | ssl_context=None, 39 | ): 40 | with patch( 41 | 'trio._highlevel_open_tcp_stream.getaddrinfo', 42 | new=getaddrinfo_async_mock(trio.socket.getaddrinfo), 43 | ): 44 | url = URL(url) 45 | 46 | dest_host = url.host 47 | if resolve_host: 48 | resolver = Resolver() 49 | _, dest_host = await resolver.resolve(url.host) 50 | 51 | if url.scheme == 'https': 52 | dest_ssl = ssl_context 53 | else: 54 | dest_ssl = None 55 | 56 | stream = await proxy.connect( 57 | dest_host=dest_host, 58 | dest_port=url.port, 59 | dest_ssl=dest_ssl, 60 | timeout=timeout, 61 | ) 62 | 63 | # fmt: off 64 | request = ( 65 | 'GET {rel_url} HTTP/1.1\r\n' 66 | 'Host: {host}\r\n' 67 | 'Connection: close\r\n\r\n' 68 | ) 69 | # fmt: on 70 | 71 | request = request.format(rel_url=url.path_qs, host=url.host) 72 | request = request.encode('ascii') 73 | 74 | await stream.write_all(request) 75 | 76 | response = await stream.read(1024) 77 | 78 | status_line = response.split(b'\r\n', 1)[0] 79 | version, status_code, *reason = status_line.split() 80 | 81 | await stream.close() 82 | 83 | return int(status_code) 84 | 85 | 86 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 87 | @pytest.mark.parametrize('rdns', (True, False)) 88 | @pytest.mark.parametrize('resolve_host', (True, False)) 89 | @pytest.mark.trio 90 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 91 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 92 | status_code = await make_request( 93 | proxy=proxy, 94 | url=url, 95 | resolve_host=resolve_host, 96 | ssl_context=target_ssl_context, 97 | ) 98 | assert status_code == 200 99 | 100 | 101 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 102 | @pytest.mark.trio 103 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 104 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 105 | status_code = await make_request( 106 | proxy=proxy, 107 | url=url, 108 | ssl_context=target_ssl_context, 109 | ) 110 | assert status_code == 200 111 | 112 | 113 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 114 | @pytest.mark.parametrize('rdns', (None, True, False)) 115 | @pytest.mark.trio 116 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 117 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 118 | status_code = await make_request( 119 | proxy=proxy, 120 | url=url, 121 | ssl_context=target_ssl_context, 122 | ) 123 | assert status_code == 200 124 | 125 | 126 | @pytest.mark.trio 127 | async def test_socks5_proxy_with_invalid_credentials(): 128 | proxy = Proxy.create( 129 | proxy_type=ProxyType.SOCKS5, 130 | host=PROXY_HOST_IPV4, 131 | port=SOCKS5_PROXY_PORT, 132 | username=LOGIN, 133 | password=PASSWORD + 'aaa', 134 | ) 135 | with pytest.raises(ProxyError): 136 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 137 | 138 | 139 | @pytest.mark.trio 140 | async def test_socks5_proxy_with_connect_timeout(): 141 | proxy = Proxy.create( 142 | proxy_type=ProxyType.SOCKS5, 143 | host=PROXY_HOST_IPV4, 144 | port=SOCKS5_PROXY_PORT, 145 | username=LOGIN, 146 | password=PASSWORD, 147 | ) 148 | with pytest.raises(ProxyTimeoutError): 149 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 150 | 151 | 152 | @pytest.mark.trio 153 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 154 | proxy = Proxy.create( 155 | proxy_type=ProxyType.SOCKS5, 156 | host=PROXY_HOST_IPV4, 157 | port=unused_tcp_port, 158 | username=LOGIN, 159 | password=PASSWORD, 160 | ) 161 | with pytest.raises(ProxyConnectionError): 162 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 163 | 164 | 165 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 166 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 167 | @pytest.mark.trio 168 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 169 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 170 | status_code = await make_request( 171 | proxy=proxy, 172 | url=url, 173 | ssl_context=target_ssl_context, 174 | ) 175 | assert status_code == 200 176 | 177 | 178 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 179 | @pytest.mark.parametrize('rdns', (None, True, False)) 180 | @pytest.mark.parametrize('resolve_host', (True, False)) 181 | @pytest.mark.trio 182 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 183 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 184 | status_code = await make_request( 185 | proxy=proxy, 186 | url=url, 187 | resolve_host=resolve_host, 188 | ssl_context=target_ssl_context, 189 | ) 190 | assert status_code == 200 191 | 192 | 193 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 194 | @pytest.mark.trio 195 | async def test_http_proxy(url, target_ssl_context): 196 | proxy = Proxy.from_url(HTTP_PROXY_URL) 197 | status_code = await make_request( 198 | proxy=proxy, 199 | url=url, 200 | ssl_context=target_ssl_context, 201 | ) 202 | assert status_code == 200 203 | 204 | 205 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 206 | @pytest.mark.trio 207 | async def test_secure_proxy(url, target_ssl_context, proxy_ssl_context): 208 | proxy = Proxy.from_url(HTTPS_PROXY_URL, proxy_ssl=proxy_ssl_context) 209 | status_code = await make_request( 210 | proxy=proxy, 211 | url=url, 212 | ssl_context=target_ssl_context, 213 | ) 214 | assert status_code == 200 215 | 216 | 217 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 218 | @pytest.mark.trio 219 | async def test_proxy_chain(url, target_ssl_context): 220 | proxy = ProxyChain( 221 | [ 222 | Proxy.from_url(SOCKS5_IPV4_URL), 223 | Proxy.from_url(SOCKS4_URL), 224 | Proxy.from_url(HTTP_PROXY_URL), 225 | ] 226 | ) 227 | status_code = await make_request( 228 | proxy=proxy, # type: ignore 229 | url=url, 230 | ssl_context=target_ssl_context, 231 | ) 232 | assert status_code == 200 233 | -------------------------------------------------------------------------------- /tests/test_proxy_async_anyio_v2.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | from yarl import URL 5 | 6 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 7 | from tests.config import ( 8 | PROXY_HOST_IPV4, 9 | SOCKS5_PROXY_PORT, 10 | LOGIN, 11 | PASSWORD, 12 | SKIP_IPV6_TESTS, 13 | SOCKS5_IPV4_URL, 14 | SOCKS5_IPV4_URL_WO_AUTH, 15 | SOCKS5_IPV6_URL, 16 | SOCKS4_URL, 17 | HTTP_PROXY_URL, 18 | TEST_URL_IPV4, 19 | SOCKS5_IPV4_HOSTNAME_URL, 20 | TEST_URL_IPV4_HTTPS, 21 | HTTPS_PROXY_URL, 22 | ) 23 | from tests.mocks import getaddrinfo_async_mock 24 | 25 | anyio = pytest.importorskip('anyio') 26 | 27 | from python_socks.async_.anyio._resolver import Resolver # noqa: E402 28 | from python_socks.async_.anyio.v2 import Proxy # noqa: E402 29 | from python_socks.async_.anyio.v2 import ProxyChain # noqa: E402 30 | from python_socks.async_.anyio.v2._proxy import AnyioProxy # noqa: E402 31 | 32 | 33 | async def make_request( 34 | proxy: AnyioProxy, 35 | url: str, 36 | resolve_host=False, 37 | timeout=None, 38 | ssl_context=None, 39 | ): 40 | # import anyio 41 | with patch( 42 | 'anyio._core._sockets.getaddrinfo', 43 | new=getaddrinfo_async_mock(anyio.getaddrinfo), 44 | ): 45 | url = URL(url) 46 | 47 | dest_host = url.host 48 | if resolve_host: 49 | resolver = Resolver() 50 | _, dest_host = await resolver.resolve(url.host) 51 | 52 | dest_ssl = None 53 | if url.scheme == 'https': 54 | dest_ssl = ssl_context 55 | 56 | stream = await proxy.connect( 57 | dest_host=dest_host, 58 | dest_port=url.port, 59 | dest_ssl=dest_ssl, 60 | timeout=timeout, 61 | ) 62 | 63 | # fmt: off 64 | request = ( 65 | 'GET {rel_url} HTTP/1.1\r\n' 66 | 'Host: {host}\r\n' 67 | 'Connection: close\r\n\r\n' 68 | ) 69 | # fmt: on 70 | 71 | request = request.format(rel_url=url.path_qs, host=url.host) 72 | request = request.encode('ascii') 73 | 74 | await stream.write_all(request) 75 | 76 | response = await stream.read(1024) 77 | 78 | status_line = response.split(b'\r\n', 1)[0] 79 | version, status_code, *reason = status_line.split() 80 | 81 | await stream.close() 82 | 83 | return int(status_code) 84 | 85 | 86 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 87 | @pytest.mark.parametrize('rdns', (True, False)) 88 | @pytest.mark.parametrize('resolve_host', (True, False)) 89 | @pytest.mark.anyio 90 | async def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 91 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 92 | status_code = await make_request( 93 | proxy=proxy, 94 | url=url, 95 | resolve_host=resolve_host, 96 | ssl_context=target_ssl_context, 97 | ) 98 | assert status_code == 200 99 | 100 | 101 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 102 | @pytest.mark.anyio 103 | async def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 104 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 105 | status_code = await make_request( 106 | proxy=proxy, 107 | url=url, 108 | ssl_context=target_ssl_context, 109 | ) 110 | assert status_code == 200 111 | 112 | 113 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 114 | @pytest.mark.parametrize('rdns', (None, True, False)) 115 | @pytest.mark.anyio 116 | async def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 117 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 118 | status_code = await make_request( 119 | proxy=proxy, 120 | url=url, 121 | ssl_context=target_ssl_context, 122 | ) 123 | assert status_code == 200 124 | 125 | 126 | @pytest.mark.anyio 127 | async def test_socks5_proxy_with_invalid_credentials(): 128 | proxy = Proxy.create( 129 | proxy_type=ProxyType.SOCKS5, 130 | host=PROXY_HOST_IPV4, 131 | port=SOCKS5_PROXY_PORT, 132 | username=LOGIN, 133 | password=PASSWORD + 'aaa', 134 | ) 135 | with pytest.raises(ProxyError): 136 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 137 | 138 | 139 | @pytest.mark.anyio 140 | async def test_socks5_proxy_with_connect_timeout(): 141 | proxy = Proxy.create( 142 | proxy_type=ProxyType.SOCKS5, 143 | host=PROXY_HOST_IPV4, 144 | port=SOCKS5_PROXY_PORT, 145 | username=LOGIN, 146 | password=PASSWORD, 147 | ) 148 | with pytest.raises(ProxyTimeoutError): 149 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 150 | 151 | 152 | @pytest.mark.anyio 153 | async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 154 | proxy = Proxy.create( 155 | proxy_type=ProxyType.SOCKS5, 156 | host=PROXY_HOST_IPV4, 157 | port=unused_tcp_port, 158 | username=LOGIN, 159 | password=PASSWORD, 160 | ) 161 | with pytest.raises(ProxyConnectionError): 162 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 163 | 164 | 165 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 166 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 167 | @pytest.mark.anyio 168 | async def test_socks5_proxy_ipv6(url, target_ssl_context): 169 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 170 | status_code = await make_request( 171 | proxy=proxy, 172 | url=url, 173 | ssl_context=target_ssl_context, 174 | ) 175 | assert status_code == 200 176 | 177 | 178 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 179 | @pytest.mark.parametrize('rdns', (None, True, False)) 180 | @pytest.mark.parametrize('resolve_host', (True, False)) 181 | @pytest.mark.anyio 182 | async def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 183 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 184 | status_code = await make_request( 185 | proxy=proxy, 186 | url=url, 187 | resolve_host=resolve_host, 188 | ssl_context=target_ssl_context, 189 | ) 190 | assert status_code == 200 191 | 192 | 193 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 194 | @pytest.mark.anyio 195 | async def test_http_proxy(url, target_ssl_context): 196 | proxy = Proxy.from_url(HTTP_PROXY_URL) 197 | status_code = await make_request( 198 | proxy=proxy, 199 | url=url, 200 | ssl_context=target_ssl_context, 201 | ) 202 | assert status_code == 200 203 | 204 | 205 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 206 | @pytest.mark.anyio 207 | async def test_secure_proxy(url, target_ssl_context, proxy_ssl_context): 208 | proxy = Proxy.from_url(HTTPS_PROXY_URL, proxy_ssl=proxy_ssl_context) 209 | status_code = await make_request( 210 | proxy=proxy, 211 | url=url, 212 | ssl_context=target_ssl_context, 213 | ) 214 | assert status_code == 200 215 | 216 | 217 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 218 | @pytest.mark.anyio 219 | async def test_proxy_chain(url, target_ssl_context): 220 | proxy = ProxyChain( 221 | [ 222 | Proxy.from_url(SOCKS5_IPV4_URL), 223 | Proxy.from_url(SOCKS4_URL), 224 | Proxy.from_url(HTTP_PROXY_URL), 225 | ] 226 | ) 227 | status_code = await make_request( 228 | proxy=proxy, # type: ignore 229 | url=url, 230 | ssl_context=target_ssl_context, 231 | ) 232 | assert status_code == 200 233 | -------------------------------------------------------------------------------- /tests/test_proxy_sync_v2.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from typing import Union 3 | from unittest import mock 4 | 5 | import pytest 6 | from yarl import URL 7 | 8 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 9 | from python_socks.sync._resolver import SyncResolver 10 | from python_socks.sync.v2 import Proxy 11 | from python_socks.sync.v2 import ProxyChain 12 | from python_socks.sync.v2._proxy import SyncProxy 13 | from tests.config import ( 14 | PROXY_HOST_IPV4, 15 | SOCKS5_PROXY_PORT, 16 | LOGIN, 17 | PASSWORD, 18 | SKIP_IPV6_TESTS, 19 | SOCKS5_IPV4_URL, 20 | SOCKS5_IPV4_URL_WO_AUTH, 21 | SOCKS5_IPV6_URL, 22 | SOCKS4_URL, 23 | HTTP_PROXY_URL, 24 | HTTP_PROXY_PORT, 25 | TEST_URL_IPV4, 26 | TEST_URL_IPv6, 27 | SOCKS5_IPV4_HOSTNAME_URL, 28 | TEST_URL_IPV4_HTTPS, 29 | HTTPS_PROXY_URL, 30 | ) 31 | from tests.mocks import getaddrinfo_sync_mock 32 | 33 | 34 | def read_status_code(sock: socket.socket) -> int: 35 | data = sock.recv(1024) 36 | status_line = data.split(b'\r\n', 1)[0] 37 | status_line = status_line.decode('utf-8', 'surrogateescape') 38 | version, status_code, *reason = status_line.split() 39 | return int(status_code) 40 | 41 | 42 | def make_request( 43 | proxy: Union[SyncProxy, ProxyChain], 44 | url: str, 45 | resolve_host=False, 46 | timeout=None, 47 | ssl_context=None, 48 | ): 49 | with mock.patch('socket.getaddrinfo', new=getaddrinfo_sync_mock()): 50 | url = URL(url) 51 | 52 | dest_host = url.host 53 | if resolve_host: 54 | resolver = SyncResolver() 55 | _, dest_host = resolver.resolve(url.host) 56 | 57 | if url.scheme == 'https': 58 | dest_ssl = ssl_context 59 | else: 60 | dest_ssl = None 61 | 62 | stream = proxy.connect( 63 | dest_host=dest_host, 64 | dest_port=url.port, 65 | dest_ssl=dest_ssl, 66 | timeout=timeout, 67 | ) 68 | 69 | # fmt: off 70 | request = ( 71 | 'GET {rel_url} HTTP/1.1\r\n' 72 | 'Host: {host}\r\n' 73 | 'Connection: close\r\n\r\n' 74 | ) 75 | # fmt: on 76 | 77 | request = request.format(rel_url=url.path_qs, host=url.host) 78 | request = request.encode('ascii') 79 | 80 | stream.write_all(request) 81 | 82 | response = stream.read(1024) 83 | 84 | status_line = response.split(b'\r\n', 1)[0] 85 | version, status_code, *reason = status_line.split() 86 | 87 | stream.close() 88 | 89 | return int(status_code) 90 | 91 | 92 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 93 | @pytest.mark.parametrize('rdns', (True, False)) 94 | @pytest.mark.parametrize('resolve_host', (True, False)) 95 | def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 96 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 97 | status_code = make_request( 98 | proxy=proxy, 99 | url=url, 100 | resolve_host=resolve_host, 101 | ssl_context=target_ssl_context, 102 | ) 103 | assert status_code == 200 104 | 105 | 106 | def test_socks5_proxy_hostname_ipv4(): 107 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 108 | status_code = make_request( 109 | proxy=proxy, 110 | url=TEST_URL_IPV4, 111 | ) 112 | assert status_code == 200 113 | 114 | 115 | @pytest.mark.parametrize('rdns', (None, True, False)) 116 | def test_socks5_proxy_ipv4_with_auth_none(rdns): 117 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 118 | status_code = make_request(proxy=proxy, url=TEST_URL_IPV4) 119 | assert status_code == 200 120 | 121 | 122 | def test_socks5_proxy_with_invalid_credentials(): 123 | proxy = Proxy.create( 124 | proxy_type=ProxyType.SOCKS5, 125 | host=PROXY_HOST_IPV4, 126 | port=SOCKS5_PROXY_PORT, 127 | username=LOGIN, 128 | password=PASSWORD + 'aaa', 129 | ) 130 | with pytest.raises(ProxyError): 131 | make_request(proxy=proxy, url=TEST_URL_IPV4) 132 | 133 | 134 | def test_socks5_proxy_with_connect_timeout(): 135 | proxy = Proxy.create( 136 | proxy_type=ProxyType.SOCKS5, 137 | host=PROXY_HOST_IPV4, 138 | port=SOCKS5_PROXY_PORT, 139 | username=LOGIN, 140 | password=PASSWORD, 141 | ) 142 | with pytest.raises(ProxyTimeoutError): 143 | make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.001) 144 | 145 | 146 | def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 147 | proxy = Proxy.create( 148 | proxy_type=ProxyType.SOCKS5, 149 | host=PROXY_HOST_IPV4, 150 | port=unused_tcp_port, 151 | username=LOGIN, 152 | password=PASSWORD, 153 | ) 154 | with pytest.raises(ProxyConnectionError): 155 | make_request(proxy=proxy, url=TEST_URL_IPV4) 156 | 157 | 158 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 159 | def test_socks5_proxy_ipv6(): 160 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 161 | status_code = make_request(proxy=proxy, url=TEST_URL_IPV4) 162 | assert status_code == 200 163 | 164 | 165 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 166 | @pytest.mark.parametrize('rdns', (True, False)) 167 | def test_socks5_proxy_hostname_ipv6(rdns): 168 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 169 | status_code = make_request(proxy=proxy, url=TEST_URL_IPv6) 170 | assert status_code == 200 171 | 172 | 173 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 174 | @pytest.mark.parametrize('rdns', (None, True, False)) 175 | @pytest.mark.parametrize('resolve_host', (True, False)) 176 | def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 177 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 178 | status_code = make_request( 179 | proxy=proxy, 180 | url=url, 181 | resolve_host=resolve_host, 182 | ssl_context=target_ssl_context, 183 | ) 184 | assert status_code == 200 185 | 186 | 187 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 188 | def test_http_proxy(url, target_ssl_context): 189 | proxy = Proxy.from_url(HTTP_PROXY_URL) 190 | status_code = make_request( 191 | proxy=proxy, 192 | url=url, 193 | ssl_context=target_ssl_context, 194 | ) 195 | assert status_code == 200 196 | 197 | 198 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 199 | def test_secure_proxy(url, target_ssl_context, proxy_ssl_context): 200 | proxy = Proxy.from_url(HTTPS_PROXY_URL, proxy_ssl=proxy_ssl_context) 201 | status_code = make_request( 202 | proxy=proxy, 203 | url=url, 204 | ssl_context=target_ssl_context, 205 | ) 206 | assert status_code == 200 207 | 208 | 209 | def test_http_proxy_with_invalid_credentials(): 210 | proxy = Proxy.create( 211 | proxy_type=ProxyType.HTTP, 212 | host=PROXY_HOST_IPV4, 213 | port=HTTP_PROXY_PORT, 214 | username=LOGIN, 215 | password=PASSWORD + 'aaa', 216 | ) 217 | with pytest.raises(ProxyError): 218 | make_request(proxy=proxy, url=TEST_URL_IPV4) 219 | 220 | 221 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 222 | def test_proxy_chain(url, target_ssl_context): 223 | proxy = ProxyChain( 224 | [ 225 | Proxy.from_url(SOCKS5_IPV4_URL), 226 | Proxy.from_url(SOCKS4_URL), 227 | Proxy.from_url(HTTP_PROXY_URL), 228 | ] 229 | ) 230 | status_code = make_request( 231 | proxy=proxy, 232 | url=url, 233 | ssl_context=target_ssl_context, 234 | ) 235 | assert status_code == 200 236 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from contextlib import contextmanager 3 | from unittest import mock 4 | 5 | import pytest 6 | import trustme 7 | 8 | from python_socks.async_.asyncio._resolver import Resolver as AsyncioResolver 9 | from python_socks.sync._resolver import SyncResolver 10 | from tests.config import ( 11 | PROXY_HOST_IPV4, 12 | PROXY_HOST_IPV6, 13 | PROXY_HOST_NAME_IPV4, 14 | PROXY_HOST_NAME_IPV6, 15 | SOCKS5_PROXY_PORT, 16 | LOGIN, 17 | PASSWORD, 18 | SKIP_IPV6_TESTS, 19 | HTTP_PROXY_PORT, 20 | SOCKS4_PORT_NO_AUTH, 21 | SOCKS4_PROXY_PORT, 22 | SOCKS5_PROXY_PORT_NO_AUTH, 23 | TEST_PORT_IPV4, 24 | TEST_PORT_IPV6, 25 | TEST_HOST_IPV4, 26 | TEST_HOST_IPV6, 27 | TEST_HOST_NAME_IPV4, 28 | TEST_HOST_NAME_IPV6, 29 | TEST_PORT_IPV4_HTTPS, 30 | HTTPS_PROXY_PORT, 31 | ) 32 | from tests.http_server import HttpServer, HttpServerConfig 33 | from tests.mocks import sync_resolve_factory, async_resolve_factory 34 | from tests.proxy_server import ProxyConfig, ProxyServer 35 | from tests.utils import wait_until_connectable 36 | 37 | 38 | @contextmanager 39 | def nullcontext(): 40 | yield None 41 | 42 | 43 | @pytest.fixture(scope='session') 44 | def target_ssl_ca() -> trustme.CA: 45 | return trustme.CA() 46 | 47 | 48 | @pytest.fixture(scope='session') 49 | def target_ssl_cert(target_ssl_ca) -> trustme.LeafCert: 50 | return target_ssl_ca.issue_cert( 51 | 'localhost', 52 | TEST_HOST_IPV4, 53 | TEST_HOST_IPV6, 54 | TEST_HOST_NAME_IPV4, 55 | TEST_HOST_NAME_IPV6, 56 | ) 57 | 58 | 59 | @pytest.fixture(scope='session') 60 | def target_ssl_certfile(target_ssl_cert): 61 | with target_ssl_cert.cert_chain_pems[0].tempfile() as cert_path: 62 | yield cert_path 63 | 64 | 65 | @pytest.fixture(scope='session') 66 | def target_ssl_keyfile(target_ssl_cert): 67 | with target_ssl_cert.private_key_pem.tempfile() as private_key_path: 68 | yield private_key_path 69 | 70 | 71 | @pytest.fixture(scope='session') 72 | def target_ssl_context(target_ssl_ca) -> ssl.SSLContext: 73 | ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 74 | ssl_ctx.verify_mode = ssl.CERT_REQUIRED 75 | ssl_ctx.check_hostname = True 76 | target_ssl_ca.configure_trust(ssl_ctx) 77 | return ssl_ctx 78 | 79 | 80 | @pytest.fixture(scope='session') 81 | def proxy_ssl_ca() -> trustme.CA: 82 | return trustme.CA() 83 | 84 | 85 | @pytest.fixture(scope='session') 86 | def proxy_ssl_cert(proxy_ssl_ca) -> trustme.LeafCert: 87 | return proxy_ssl_ca.issue_cert( 88 | 'localhost', 89 | PROXY_HOST_IPV4, 90 | PROXY_HOST_IPV6, 91 | PROXY_HOST_NAME_IPV4, 92 | PROXY_HOST_NAME_IPV6, 93 | ) 94 | 95 | 96 | @pytest.fixture(scope='session') 97 | def proxy_ssl_context(proxy_ssl_ca) -> ssl.SSLContext: 98 | ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) 99 | ssl_ctx.verify_mode = ssl.CERT_REQUIRED 100 | ssl_ctx.check_hostname = True 101 | proxy_ssl_ca.configure_trust(ssl_ctx) 102 | return ssl_ctx 103 | 104 | 105 | @pytest.fixture(scope='session') 106 | def proxy_ssl_certfile(proxy_ssl_cert): 107 | with proxy_ssl_cert.cert_chain_pems[0].tempfile() as cert_path: 108 | yield cert_path 109 | 110 | 111 | @pytest.fixture(scope='session') 112 | def proxy_ssl_keyfile(proxy_ssl_cert): 113 | with proxy_ssl_cert.private_key_pem.tempfile() as private_key_path: 114 | yield private_key_path 115 | 116 | 117 | @pytest.fixture(scope='session', autouse=True) 118 | def patch_resolvers(): 119 | p1 = mock.patch.object( 120 | SyncResolver, 121 | attribute='resolve', 122 | new=sync_resolve_factory(SyncResolver), 123 | ) 124 | 125 | p2 = mock.patch.object( 126 | AsyncioResolver, 127 | attribute='resolve', 128 | new=async_resolve_factory(AsyncioResolver), 129 | ) 130 | 131 | try: 132 | # noinspection PyProtectedMember 133 | from python_socks.async_.trio._resolver import Resolver as TrioResolver 134 | except ImportError: 135 | p3 = nullcontext() 136 | else: 137 | p3 = mock.patch.object( 138 | TrioResolver, 139 | attribute='resolve', 140 | new=async_resolve_factory(TrioResolver), 141 | ) 142 | 143 | try: 144 | # noinspection PyProtectedMember 145 | from python_socks.async_.curio._resolver import Resolver as CurioResolver 146 | except ImportError: 147 | p4 = nullcontext() 148 | else: 149 | p4 = mock.patch.object( 150 | CurioResolver, 151 | attribute='resolve', 152 | new=async_resolve_factory(CurioResolver), 153 | ) 154 | 155 | try: 156 | from python_socks.async_.anyio._resolver import Resolver as AnyioResolver 157 | except ImportError: 158 | p5 = nullcontext() 159 | else: 160 | p5 = mock.patch.object( 161 | AnyioResolver, 162 | attribute='resolve', 163 | new=async_resolve_factory(AnyioResolver), 164 | ) 165 | 166 | with p1, p2, p3, p4, p5: 167 | yield None 168 | 169 | 170 | @pytest.fixture(scope='session', autouse=True) 171 | def proxy_server(proxy_ssl_certfile, proxy_ssl_keyfile): 172 | config = [ 173 | ProxyConfig( 174 | proxy_type='http', 175 | host=PROXY_HOST_IPV4, 176 | port=HTTP_PROXY_PORT, 177 | username=LOGIN, 178 | password=PASSWORD, 179 | ), 180 | ProxyConfig( 181 | proxy_type='socks4', 182 | host=PROXY_HOST_IPV4, 183 | port=SOCKS4_PROXY_PORT, 184 | username=LOGIN, 185 | password=None, 186 | ), 187 | ProxyConfig( 188 | proxy_type='socks4', 189 | host=PROXY_HOST_IPV4, 190 | port=SOCKS4_PORT_NO_AUTH, 191 | username=None, 192 | password=None, 193 | ), 194 | ProxyConfig( 195 | proxy_type='socks5', 196 | host=PROXY_HOST_IPV4, 197 | port=SOCKS5_PROXY_PORT, 198 | username=LOGIN, 199 | password=PASSWORD, 200 | ), 201 | ProxyConfig( 202 | proxy_type='socks5', 203 | host=PROXY_HOST_IPV4, 204 | port=SOCKS5_PROXY_PORT_NO_AUTH, 205 | username=None, 206 | password=None, 207 | ), 208 | ProxyConfig( 209 | proxy_type='http', 210 | # host=PROXY_HOST_NAME_IPV4, 211 | host=PROXY_HOST_IPV4, 212 | port=HTTPS_PROXY_PORT, 213 | username=LOGIN, 214 | password=PASSWORD, 215 | ssl_certfile=proxy_ssl_certfile, 216 | ssl_keyfile=proxy_ssl_keyfile, 217 | ), 218 | ] 219 | 220 | if not SKIP_IPV6_TESTS: 221 | config.append( 222 | ProxyConfig( 223 | proxy_type='socks5', 224 | host=PROXY_HOST_IPV6, 225 | port=SOCKS5_PROXY_PORT, 226 | username=LOGIN, 227 | password=PASSWORD, 228 | ), 229 | ) 230 | 231 | server = ProxyServer(config=config) 232 | server.start() 233 | for cfg in config: 234 | wait_until_connectable(host=cfg.host, port=cfg.port, timeout=10) 235 | 236 | yield None 237 | 238 | server.terminate() 239 | 240 | 241 | @pytest.fixture(scope='session', autouse=True) 242 | def web_server(target_ssl_certfile, target_ssl_keyfile): 243 | config = [ 244 | HttpServerConfig( 245 | host=TEST_HOST_IPV4, 246 | port=TEST_PORT_IPV4, 247 | ), 248 | HttpServerConfig( 249 | host=TEST_HOST_IPV4, 250 | port=TEST_PORT_IPV4_HTTPS, 251 | # certfile=TEST_HOST_CERT_FILE, 252 | # keyfile=TEST_HOST_KEY_FILE, 253 | certfile=target_ssl_certfile, 254 | keyfile=target_ssl_keyfile, 255 | ), 256 | ] 257 | 258 | if not SKIP_IPV6_TESTS: 259 | config.append(HttpServerConfig(host=TEST_HOST_IPV6, port=TEST_PORT_IPV6)) 260 | 261 | server = HttpServer(config=config) 262 | server.start() 263 | for cfg in config: 264 | server.wait_until_connectable(host=cfg.host, port=cfg.port) 265 | 266 | yield None 267 | 268 | server.terminate() 269 | -------------------------------------------------------------------------------- /tests/test_proxy_async_curio.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | from yarl import URL 6 | 7 | from python_socks import ProxyType, ProxyError, ProxyTimeoutError, ProxyConnectionError 8 | from python_socks.async_ import ProxyChain 9 | from tests.config import ( 10 | PROXY_HOST_IPV4, 11 | SOCKS5_PROXY_PORT, 12 | LOGIN, 13 | PASSWORD, 14 | SKIP_IPV6_TESTS, 15 | SOCKS5_IPV4_URL, 16 | SOCKS5_IPV4_URL_WO_AUTH, 17 | SOCKS5_IPV6_URL, 18 | SOCKS4_URL, 19 | HTTP_PROXY_URL, 20 | TEST_URL_IPV4, 21 | SOCKS5_IPV4_HOSTNAME_URL, 22 | TEST_URL_IPV4_HTTPS, 23 | ) 24 | from tests.mocks import getaddrinfo_async_mock 25 | 26 | curio = pytest.importorskip('curio') 27 | 28 | import curio.io # noqa: E402 29 | import curio.ssl as curiossl # noqa: E402 30 | import curio.socket # noqa: E402 31 | from python_socks.async_.curio._resolver import Resolver # noqa: E402 32 | from python_socks.async_.curio import Proxy # noqa: E402 33 | from python_socks.async_.curio._proxy import CurioProxy # noqa: E402 34 | 35 | 36 | async def make_request( 37 | proxy: CurioProxy, 38 | url: str, 39 | resolve_host=False, 40 | timeout=None, 41 | ssl_context=None, 42 | ): 43 | with patch( 44 | 'curio.socket.getaddrinfo', 45 | new=getaddrinfo_async_mock(curio.socket.getaddrinfo), 46 | ): 47 | url = URL(url) 48 | 49 | dest_host = url.host 50 | if resolve_host: 51 | resolver = Resolver() 52 | _, dest_host = await resolver.resolve(url.host) 53 | 54 | sock: curio.io.Socket = await proxy.connect( 55 | dest_host=dest_host, dest_port=url.port, timeout=timeout 56 | ) 57 | 58 | dest_ssl: Optional[curiossl.CurioSSLContext] = None 59 | if url.scheme == 'https': 60 | dest_ssl = curiossl.CurioSSLContext(ssl_context) 61 | 62 | if dest_ssl is not None: 63 | sock = await dest_ssl.wrap_socket( 64 | sock, 65 | do_handshake_on_connect=False, 66 | server_hostname=url.host, 67 | ) 68 | 69 | await sock.do_handshake() 70 | 71 | stream = sock.as_stream() 72 | 73 | # fmt: off 74 | request = ( 75 | 'GET {rel_url} HTTP/1.1\r\n' 76 | 'Host: {host}\r\n' 77 | 'Connection: close\r\n\r\n' 78 | ) 79 | # fmt: on 80 | 81 | request = request.format(rel_url=url.path_qs, host=url.host) 82 | request = request.encode('ascii') 83 | 84 | await stream.write(request) 85 | 86 | response = await stream.read(1024) 87 | 88 | status_line = response.split(b'\r\n', 1)[0] 89 | status_line = status_line.decode('utf-8', 'surrogateescape') 90 | version, status_code, *reason = status_line.split() 91 | 92 | return int(status_code) 93 | 94 | 95 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 96 | @pytest.mark.parametrize('rdns', (True, False)) 97 | @pytest.mark.parametrize('resolve_host', (True, False)) 98 | def test_socks5_proxy_ipv4(url, rdns, resolve_host, target_ssl_context): 99 | async def main(): 100 | proxy = Proxy.from_url(SOCKS5_IPV4_URL, rdns=rdns) 101 | status_code = await make_request( 102 | proxy=proxy, 103 | url=url, 104 | resolve_host=resolve_host, 105 | ssl_context=target_ssl_context, 106 | ) 107 | assert status_code == 200 108 | 109 | curio.run(main) 110 | 111 | 112 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 113 | def test_socks5_proxy_hostname_ipv4(url, target_ssl_context): 114 | async def main(): 115 | proxy = Proxy.from_url(SOCKS5_IPV4_HOSTNAME_URL) 116 | status_code = await make_request( 117 | proxy=proxy, 118 | url=url, 119 | ssl_context=target_ssl_context, 120 | ) 121 | assert status_code == 200 122 | 123 | curio.run(main) 124 | 125 | 126 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 127 | @pytest.mark.parametrize('rdns', (None, True, False)) 128 | def test_socks5_proxy_ipv4_with_auth_none(url, rdns, target_ssl_context): 129 | async def main(): 130 | proxy = Proxy.from_url(SOCKS5_IPV4_URL_WO_AUTH, rdns=rdns) 131 | status_code = await make_request( 132 | proxy=proxy, 133 | url=url, 134 | ssl_context=target_ssl_context, 135 | ) 136 | assert status_code == 200 137 | 138 | curio.run(main) 139 | 140 | 141 | def test_socks5_proxy_with_invalid_credentials(): 142 | async def main(): 143 | proxy = Proxy.create( 144 | proxy_type=ProxyType.SOCKS5, 145 | host=PROXY_HOST_IPV4, 146 | port=SOCKS5_PROXY_PORT, 147 | username=LOGIN, 148 | password=PASSWORD + 'aaa', 149 | ) 150 | with pytest.raises(ProxyError): 151 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 152 | 153 | curio.run(main) 154 | 155 | 156 | def test_socks5_proxy_with_connect_timeout(): 157 | async def main(): 158 | proxy = Proxy.create( 159 | proxy_type=ProxyType.SOCKS5, 160 | host=PROXY_HOST_IPV4, 161 | port=SOCKS5_PROXY_PORT, 162 | username=LOGIN, 163 | password=PASSWORD, 164 | ) 165 | with pytest.raises(ProxyTimeoutError): 166 | await make_request(proxy=proxy, url=TEST_URL_IPV4, timeout=0.0001) 167 | 168 | curio.run(main) 169 | 170 | 171 | def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port): 172 | async def main(): 173 | proxy = Proxy.create( 174 | proxy_type=ProxyType.SOCKS5, 175 | host=PROXY_HOST_IPV4, 176 | port=unused_tcp_port, 177 | username=LOGIN, 178 | password=PASSWORD, 179 | ) 180 | with pytest.raises(ProxyConnectionError): 181 | await make_request(proxy=proxy, url=TEST_URL_IPV4) 182 | 183 | curio.run(main) 184 | 185 | 186 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 187 | @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") 188 | def test_socks5_proxy_ipv6(url, target_ssl_context): 189 | async def main(): 190 | proxy = Proxy.from_url(SOCKS5_IPV6_URL) 191 | status_code = await make_request( 192 | proxy=proxy, 193 | url=url, 194 | ssl_context=target_ssl_context, 195 | ) 196 | assert status_code == 200 197 | 198 | curio.run(main) 199 | 200 | 201 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 202 | @pytest.mark.parametrize('rdns', (None, True, False)) 203 | @pytest.mark.parametrize('resolve_host', (True, False)) 204 | def test_socks4_proxy(url, rdns, resolve_host, target_ssl_context): 205 | async def main(): 206 | proxy = Proxy.from_url(SOCKS4_URL, rdns=rdns) 207 | status_code = await make_request( 208 | proxy=proxy, 209 | url=url, 210 | resolve_host=resolve_host, 211 | ssl_context=target_ssl_context, 212 | ) 213 | assert status_code == 200 214 | 215 | curio.run(main) 216 | 217 | 218 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 219 | def test_http_proxy(url, target_ssl_context): 220 | async def main(): 221 | proxy = Proxy.from_url(HTTP_PROXY_URL) 222 | status_code = await make_request( 223 | proxy=proxy, 224 | url=url, 225 | ssl_context=target_ssl_context, 226 | ) 227 | assert status_code == 200 228 | 229 | curio.run(main) 230 | 231 | 232 | @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) 233 | def test_proxy_chain(url, target_ssl_context): 234 | async def main(): 235 | proxy = ProxyChain( 236 | [ 237 | Proxy.from_url(SOCKS5_IPV4_URL), 238 | Proxy.from_url(SOCKS4_URL), 239 | Proxy.from_url(HTTP_PROXY_URL), 240 | ] 241 | ) 242 | status_code = await make_request( 243 | proxy=proxy, # type: ignore 244 | url=url, 245 | ssl_context=target_ssl_context, 246 | ) 247 | assert status_code == 200 248 | 249 | curio.run(main) 250 | --------------------------------------------------------------------------------