pax_global_header00006660000000000000000000000064145067450660014527gustar00rootroot0000000000000052 comment=adb9bb0ff29ee965a329817c977148f175fbb91b aiohttp-socks-0.8.4/000077500000000000000000000000001450674506600143305ustar00rootroot00000000000000aiohttp-socks-0.8.4/.coveragerc000066400000000000000000000004521450674506600164520ustar00rootroot00000000000000[run] omit = # */_proxy_chain_*.py aiohttp_socks/core_socks/_basic_auth.py aiohttp_socks/_deprecated.py [report] # Regexes for lines to exclude from consideration exclude_lines = pragma: no cover def __repr__ if self.debug: raise NotImplementedError raise ValueError aiohttp-socks-0.8.4/.flake8000066400000000000000000000000611450674506600155000ustar00rootroot00000000000000[flake8] ignore = N805,W503 max-line-length = 99 aiohttp-socks-0.8.4/.github/000077500000000000000000000000001450674506600156705ustar00rootroot00000000000000aiohttp-socks-0.8.4/.github/workflows/000077500000000000000000000000001450674506600177255ustar00rootroot00000000000000aiohttp-socks-0.8.4/.github/workflows/ci.yml000066400000000000000000000022421450674506600210430ustar00rootroot00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] jobs: build: name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" runs-on: "${{ matrix.os }}" strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest] steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools pip install -r requirements-dev.txt - name: Lint with flake8 run: | python -m flake8 aiohttp_socks tests continue-on-error: true - name: Run tests # run: python -m pytest tests --cov=./aiohttp_socks --cov-report term-missing -s run: python -m pytest tests --cov=./aiohttp_socks --cov-report xml - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage.xml flags: unit fail_ci_if_error: falseaiohttp-socks-0.8.4/.gitignore000066400000000000000000000005171450674506600163230ustar00rootroot00000000000000*.bak *.egg *.egg-info *.eggs *.pyc *.pyd *.pyo *.so *.tar.gz *~ .DS_Store .Python .cache .coverage .coverage.* .idea .installed.cfg .noseids .tox .vimrc # bin build cover coverage develop-eggs dist docs/_build/ eggs include/ lib/ man/ nosetests.xml parts pyvenv sources var/* venv virtualenv.py .install-deps .develop .idea/ usage*.pyaiohttp-socks-0.8.4/LICENSE.txt000066400000000000000000000261351450674506600161620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. aiohttp-socks-0.8.4/MANIFEST.in000066400000000000000000000000571450674506600160700ustar00rootroot00000000000000# Include the license file include LICENSE.txt aiohttp-socks-0.8.4/README.md000066400000000000000000000050521450674506600156110ustar00rootroot00000000000000## aiohttp-socks [![CI](https://github.com/romis2012/aiohttp-socks/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/aiohttp-socks/actions/workflows/ci.yml) [![Coverage Status](https://codecov.io/gh/romis2012/aiohttp-socks/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/aiohttp-socks) [![PyPI version](https://badge.fury.io/py/aiohttp-socks.svg)](https://pypi.python.org/pypi/aiohttp-socks) The `aiohttp-socks` package provides a proxy connector for [aiohttp](https://github.com/aio-libs/aiohttp). Supports SOCKS4(a), SOCKS5(h), HTTP (tunneling) as well as Proxy chains. It uses [python-socks](https://github.com/romis2012/python-socks) for core proxy functionality. ## Requirements - Python >= 3.6 - aiohttp >= 2.3.2 - python-socks[asyncio] >= 1.0.1 ## Installation ``` pip install aiohttp_socks ``` ## Usage #### aiohttp usage: ```python import aiohttp from aiohttp_socks import ProxyType, ProxyConnector, ChainProxyConnector async def fetch(url): connector = ProxyConnector.from_url('socks5://user:password@127.0.0.1:1080') ### or use ProxyConnector constructor # connector = ProxyConnector( # proxy_type=ProxyType.SOCKS5, # host='127.0.0.1', # port=1080, # username='user', # password='password', # rdns=True # ) ### proxy chaining (since ver 0.3.3) # connector = ChainProxyConnector.from_urls([ # 'socks5://user:password@127.0.0.1:1080', # 'socks4://127.0.0.1:1081', # 'http://user:password@127.0.0.1:3128', # ]) async with aiohttp.ClientSession(connector=connector) as session: async with session.get(url) as response: return await response.text() ``` #### aiohttp-socks also provides `open_connection` and `create_connection` functions: ```python from aiohttp_socks import open_connection async def fetch(): reader, writer = await open_connection( proxy_url='socks5://user:password@127.0.0.1:1080', host='check-host.net', port=80 ) request = (b"GET /ip HTTP/1.1\r\n" b"Host: check-host.net\r\n" b"Connection: close\r\n\r\n") writer.write(request) return await reader.read(-1) ``` ## Why yet another SOCKS connector for aiohttp Unlike [aiosocksy](https://github.com/romis2012/aiosocksy), aiohttp_socks has only single point of integration with aiohttp. This makes it easier to maintain compatibility with new aiohttp versions. aiohttp-socks-0.8.4/aiohttp_socks/000077500000000000000000000000001450674506600172025ustar00rootroot00000000000000aiohttp-socks-0.8.4/aiohttp_socks/__init__.py000066400000000000000000000013471450674506600213200ustar00rootroot00000000000000__title__ = 'aiohttp-socks' __version__ = '0.8.4' from python_socks import ( ProxyError, ProxyTimeoutError, ProxyConnectionError, ProxyType ) from .connector import ( ProxyConnector, ChainProxyConnector, ProxyInfo ) from .utils import open_connection, create_connection from ._deprecated import ( SocksVer, SocksConnector, SocksConnectionError, SocksError ) __all__ = ( '__title__', '__version__', 'ProxyConnector', 'ChainProxyConnector', 'ProxyInfo', 'ProxyType', 'ProxyError', 'ProxyConnectionError', 'ProxyTimeoutError', 'open_connection', 'create_connection', 'SocksVer', 'SocksConnector', 'SocksError', 'SocksConnectionError', ) aiohttp-socks-0.8.4/aiohttp_socks/_deprecated.py000066400000000000000000000016131450674506600220140ustar00rootroot00000000000000import warnings from python_socks import ( ProxyError, ProxyConnectionError, ProxyType ) from .connector import ProxyConnector class SocksVer(object): SOCKS4 = 1 SOCKS5 = 2 def _warn_about_connector(): warnings.warn('SocksConnector is deprecated. ' 'Use ProxyConnector instead.', DeprecationWarning, stacklevel=3) class SocksConnector(ProxyConnector): def __init__(self, socks_ver=SocksVer.SOCKS5, **kwargs): _warn_about_connector() # noqa if 'proxy_type' in kwargs: # from_url super().__init__(**kwargs) else: super().__init__(proxy_type=ProxyType(socks_ver), **kwargs) @classmethod def from_url(cls, url, **kwargs): _warn_about_connector() # noqa return super().from_url(url, **kwargs) SocksError = ProxyError SocksConnectionError = ProxyConnectionError aiohttp-socks-0.8.4/aiohttp_socks/connector.py000066400000000000000000000123241450674506600215500ustar00rootroot00000000000000import asyncio import socket import typing from asyncio import BaseTransport, StreamWriter from typing import Iterable from aiohttp import TCPConnector from aiohttp.abc import AbstractResolver from aiohttp.client_proto import ResponseHandler from python_socks import ProxyType, parse_proxy_url from python_socks.async_.asyncio.v2 import Proxy class NoResolver(AbstractResolver): async def resolve(self, host, port=0, family=socket.AF_INET): return [ { 'hostname': host, 'host': host, 'port': port, 'family': family, 'proto': 0, 'flags': 0, } ] async def close(self): pass # pragma: no cover class _ResponseHandler(ResponseHandler): """ To fix issue https://github.com/romis2012/aiohttp-socks/issues/27 In Python>=3.11.5 we need to keep a reference to the StreamWriter so that the underlying transport is not closed during garbage collection. See StreamWriter.__del__ method (was added in Python 3.11.5) """ def __init__(self, loop: asyncio.AbstractEventLoop, writer: StreamWriter): super().__init__(loop) self._writer = writer class ProxyConnector(TCPConnector): def __init__( self, proxy_type=ProxyType.SOCKS5, host=None, port=None, username=None, password=None, rdns=None, proxy_ssl=None, **kwargs, ): kwargs['resolver'] = NoResolver() super().__init__(**kwargs) self._proxy_type = proxy_type self._proxy_host = host self._proxy_port = port self._proxy_username = username self._proxy_password = password self._rdns = rdns self._proxy_ssl = proxy_ssl # noinspection PyMethodOverriding async def _wrap_create_connection(self, protocol_factory, host, port, *, ssl, **kwargs): proxy = Proxy( proxy_type=self._proxy_type, host=self._proxy_host, port=self._proxy_port, username=self._proxy_username, password=self._proxy_password, rdns=self._rdns, proxy_ssl=self._proxy_ssl, ) connect_timeout = None timeout = kwargs.get('timeout') if timeout is not None: connect_timeout = getattr(timeout, 'sock_connect', None) stream = await proxy.connect( dest_host=host, dest_port=port, dest_ssl=ssl, timeout=connect_timeout, ) transport: BaseTransport = stream.writer.transport protocol: ResponseHandler = _ResponseHandler( loop=self._loop, writer=stream.writer, ) transport.set_protocol(protocol) protocol.connection_made(transport) return transport, protocol @classmethod def from_url(cls, url, **kwargs): proxy_type, host, port, username, password = parse_proxy_url(url) return cls( proxy_type=proxy_type, host=host, port=port, username=username, password=password, **kwargs, ) class ProxyInfo(typing.NamedTuple): proxy_type: ProxyType host: str port: int username: typing.Optional[str] = None password: typing.Optional[str] = None rdns: typing.Optional[bool] = None class ChainProxyConnector(TCPConnector): def __init__(self, proxy_infos: Iterable[ProxyInfo], **kwargs): kwargs['resolver'] = NoResolver() super().__init__(**kwargs) self._proxy_infos = proxy_infos # noinspection PyMethodOverriding async def _wrap_create_connection(self, protocol_factory, host, port, *, ssl, **kwargs): forward = None proxy = None for info in self._proxy_infos: proxy = Proxy( proxy_type=info.proxy_type, host=info.host, port=info.port, username=info.username, password=info.password, rdns=info.rdns, forward=forward, ) forward = proxy connect_timeout = None timeout = kwargs.get('timeout') if timeout is not None: connect_timeout = getattr(timeout, 'sock_connect', None) stream = await proxy.connect( dest_host=host, dest_port=port, dest_ssl=ssl, timeout=connect_timeout, ) transport: BaseTransport = stream.writer.transport protocol: ResponseHandler = _ResponseHandler( loop=self._loop, writer=stream.writer, ) transport.set_protocol(protocol) protocol.connection_made(transport) return transport, protocol @classmethod def from_urls(cls, urls: Iterable[str], **kwargs): infos = [] for url in urls: proxy_type, host, port, username, password = parse_proxy_url(url) proxy_info = ProxyInfo( proxy_type=proxy_type, host=host, port=port, username=username, password=password, ) infos.append(proxy_info) return cls(infos, **kwargs) aiohttp-socks-0.8.4/aiohttp_socks/utils.py000066400000000000000000000047351450674506600207250ustar00rootroot00000000000000import asyncio import warnings from python_socks import ProxyType, parse_proxy_url from python_socks.async_.asyncio import Proxy async def open_connection( proxy_url=None, host=None, port=None, *, proxy_type=ProxyType.SOCKS5, proxy_host='127.0.0.1', proxy_port=1080, username=None, password=None, rdns=True, loop=None, **kwargs, ): warnings.warn( 'open_connection is deprecated. ' 'Use https://github.com/romis2012/python-socks directly instead.', DeprecationWarning, stacklevel=2, ) if host is None or port is None: raise ValueError('host and port must be specified') # pragma: no cover if loop is None: loop = asyncio.get_event_loop() if proxy_url is not None: proxy_type, proxy_host, proxy_port, username, password = parse_proxy_url(proxy_url) proxy = Proxy.create( proxy_type=proxy_type, host=proxy_host, port=proxy_port, username=username, password=password, rdns=rdns, loop=loop, ) sock = await proxy.connect(host, port) # noinspection PyTypeChecker return await asyncio.open_connection(host=None, port=None, sock=sock, **kwargs) async def create_connection( proxy_url=None, protocol_factory=None, host=None, port=None, *, proxy_type=ProxyType.SOCKS5, proxy_host='127.0.0.1', proxy_port=1080, username=None, password=None, rdns=True, loop=None, **kwargs, ): warnings.warn( 'create_connection is deprecated. ' 'Use https://github.com/romis2012/python-socks directly instead.', DeprecationWarning, stacklevel=2, ) if protocol_factory is None: raise ValueError('protocol_factory must be specified') # pragma: no cover if host is None or port is None: raise ValueError('host and port must be specified') # pragma: no cover if loop is None: loop = asyncio.get_event_loop() if proxy_url is not None: proxy_type, proxy_host, proxy_port, username, password = parse_proxy_url(proxy_url) proxy = Proxy.create( proxy_type=proxy_type, host=proxy_host, port=proxy_port, username=username, password=password, rdns=rdns, loop=loop, ) sock = await proxy.connect(host, port) return await loop.create_connection( protocol_factory=protocol_factory, host=None, port=None, sock=sock, **kwargs ) aiohttp-socks-0.8.4/pyproject.toml000066400000000000000000000002721450674506600172450ustar00rootroot00000000000000[tool.black] line-length = 99 target-version = ['py37', 'py38', 'py39'] skip-string-normalization = true preview = true verbose = true [tool.pytest.ini_options] asyncio_mode = 'strict' aiohttp-socks-0.8.4/requirements-dev.txt000066400000000000000000000004651450674506600203750ustar00rootroot00000000000000-e . importlib_metadata==4.12.0; python_version == "3.7" flake8==3.9.1 pytest==7.0.1 pytest-cov==3.0.0 # coveralls==3.3.1 pytest-asyncio==0.16.0; python_version < "3.7" pytest-asyncio==0.18.3; python_version >= "3.7" trustme==0.9.0 attrs>=19.3.0 yarl>=1.4.2 flask>=1.1.2 anyio>=3.3.4,<4.0.0 tiny-proxy>=0.1.1 aiohttp-socks-0.8.4/setup.py000066400000000000000000000021461450674506600160450ustar00rootroot00000000000000#!/usr/bin/env python import os import re import sys from setuptools import setup if sys.version_info < (3, 6, 0): raise RuntimeError('aiohttp-socks requires Python 3.6+') def get_version(): here = os.path.dirname(os.path.abspath(__file__)) filename = os.path.join(here, 'aiohttp_socks', '__init__.py') contents = open(filename).read() pattern = r"^__version__ = '(.*?)'$" return re.search(pattern, contents, re.MULTILINE).group(1) def get_long_description(): with open('README.md', mode='r', encoding='utf8') as f: return f.read() setup( name='aiohttp_socks', author='Roman Snegirev', author_email='snegiryev@gmail.com', version=get_version(), license='Apache 2', url='https://github.com/romis2012/aiohttp-socks', description='Proxy connector for aiohttp', long_description=get_long_description(), long_description_content_type='text/markdown', packages=['aiohttp_socks'], keywords='asyncio aiohttp socks socks5 socks4 http proxy', install_requires=[ 'aiohttp>=2.3.2', 'python-socks[asyncio]>=2.4.3,<3.0.0', ], ) aiohttp-socks-0.8.4/tests/000077500000000000000000000000001450674506600154725ustar00rootroot00000000000000aiohttp-socks-0.8.4/tests/__init__.py000066400000000000000000000000311450674506600175750ustar00rootroot00000000000000# -*- coding: utf-8 -*- aiohttp-socks-0.8.4/tests/config.py000066400000000000000000000040751450674506600173170ustar00rootroot00000000000000import os LOGIN = 'admin' PASSWORD = 'admin' PROXY_HOST_IPV4 = '127.0.0.1' PROXY_HOST_IPV6 = '::1' PROXY_HOST_NAME_IPV4 = 'ip4.proxy.example.com' PROXY_HOST_NAME_IPV6 = 'ip6.proxy.example.com' SOCKS5_PROXY_PORT = 7780 SOCKS5_PROXY_PORT_NO_AUTH = 7781 SOCKS4_PROXY_PORT = 7782 SOCKS4_PORT_NO_AUTH = 7783 HTTP_PROXY_PORT = 7784 SKIP_IPV6_TESTS = 'SKIP_IPV6_TESTS' in os.environ SOCKS5_IPV4_URL = 'socks5://{login}:{password}@{host}:{port}'.format( host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT, login=LOGIN, password=PASSWORD, ) SOCKS5_IPV6_URL = 'socks5://{login}:{password}@{host}:{port}'.format( host='[%s]' % PROXY_HOST_IPV6, port=SOCKS5_PROXY_PORT, login=LOGIN, password=PASSWORD, ) SOCKS5_IPV4_HOSTNAME_URL = 'socks5://{login}:{password}@{host}:{port}'.format( host=PROXY_HOST_NAME_IPV4, port=SOCKS5_PROXY_PORT, login=LOGIN, password=PASSWORD, ) SOCKS5_IPV4_URL_WO_AUTH = 'socks5://{host}:{port}'.format( host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT_NO_AUTH ) SOCKS4_URL = 'socks4://{login}:{password}@{host}:{port}'.format( host=PROXY_HOST_IPV4, port=SOCKS4_PROXY_PORT, login=LOGIN, password='', ) HTTP_PROXY_URL = 'http://{login}:{password}@{host}:{port}'.format( host=PROXY_HOST_IPV4, port=HTTP_PROXY_PORT, login=LOGIN, password=PASSWORD, ) TEST_HOST_IPV4 = '127.0.0.1' TEST_HOST_IPV6 = '::1' TEST_HOST_NAME_IPV4 = 'ip4.target.example.com' TEST_HOST_NAME_IPV6 = 'ip6.target.example.com' TEST_PORT_IPV4 = 8889 TEST_PORT_IPV6 = 8889 TEST_PORT_IPV4_HTTPS = 8890 TEST_URL_IPV4 = 'http://{host}:{port}/ip'.format(host=TEST_HOST_NAME_IPV4, port=TEST_PORT_IPV4) TEST_URL_IPv6 = 'http://{host}:{port}/ip'.format(host=TEST_HOST_NAME_IPV6, port=TEST_PORT_IPV6) TEST_URL_IPV4_DELAY = 'http://{host}:{port}/delay/2'.format( host=TEST_HOST_NAME_IPV4, port=TEST_PORT_IPV4 ) TEST_URL_IPV4_HTTPS = 'https://{host}:{port}/ip'.format( host=TEST_HOST_NAME_IPV4, port=TEST_PORT_IPV4_HTTPS ) def resolve_path(path): return os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), path)) aiohttp-socks-0.8.4/tests/conftest.py000066400000000000000000000101471450674506600176740ustar00rootroot00000000000000import ssl from unittest import mock import pytest # noqa import trustme # noqa from python_socks.async_.asyncio._resolver import Resolver as AsyncioResolver # noqa from tests.config import ( PROXY_HOST_IPV4, PROXY_HOST_IPV6, SOCKS5_PROXY_PORT, LOGIN, PASSWORD, SKIP_IPV6_TESTS, HTTP_PROXY_PORT, SOCKS4_PORT_NO_AUTH, SOCKS4_PROXY_PORT, SOCKS5_PROXY_PORT_NO_AUTH, TEST_PORT_IPV4, TEST_PORT_IPV6, TEST_HOST_IPV4, TEST_HOST_IPV6, TEST_PORT_IPV4_HTTPS, TEST_HOST_NAME_IPV4, TEST_HOST_NAME_IPV6, ) from tests.http_server import HttpServer, HttpServerConfig from tests.mocks import async_resolve_factory from tests.proxy_server import ProxyConfig, ProxyServer from tests.utils import wait_until_connectable @pytest.fixture(scope='session') def target_ssl_ca() -> trustme.CA: return trustme.CA() @pytest.fixture(scope='session') def target_ssl_cert(target_ssl_ca) -> trustme.LeafCert: return target_ssl_ca.issue_cert( 'localhost', TEST_HOST_IPV4, TEST_HOST_IPV6, TEST_HOST_NAME_IPV4, TEST_HOST_NAME_IPV6, ) @pytest.fixture(scope='session') def target_ssl_certfile(target_ssl_cert): with target_ssl_cert.cert_chain_pems[0].tempfile() as cert_path: yield cert_path @pytest.fixture(scope='session') def target_ssl_keyfile(target_ssl_cert): with target_ssl_cert.private_key_pem.tempfile() as private_key_path: yield private_key_path @pytest.fixture(scope='session') def target_ssl_context(target_ssl_ca) -> ssl.SSLContext: ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_ctx.verify_mode = ssl.CERT_REQUIRED ssl_ctx.check_hostname = True target_ssl_ca.configure_trust(ssl_ctx) return ssl_ctx @pytest.fixture(scope='session', autouse=True) def patch_resolvers(): with mock.patch.object( AsyncioResolver, attribute='resolve', new=async_resolve_factory(AsyncioResolver) ): yield None @pytest.fixture(scope='session', autouse=True) def proxy_server(): config = [ ProxyConfig( proxy_type='http', host=PROXY_HOST_IPV4, port=HTTP_PROXY_PORT, username=LOGIN, password=PASSWORD, ), ProxyConfig( proxy_type='socks4', host=PROXY_HOST_IPV4, port=SOCKS4_PROXY_PORT, username=LOGIN, password=None, ), ProxyConfig( proxy_type='socks4', host=PROXY_HOST_IPV4, port=SOCKS4_PORT_NO_AUTH, username=None, password=None, ), ProxyConfig( proxy_type='socks5', host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT, username=LOGIN, password=PASSWORD, ), ProxyConfig( proxy_type='socks5', host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT_NO_AUTH, username=None, password=None, ), ] if not SKIP_IPV6_TESTS: config.append( ProxyConfig( proxy_type='socks5', host=PROXY_HOST_IPV6, port=SOCKS5_PROXY_PORT, username=LOGIN, password=PASSWORD, ), ) server = ProxyServer(config=config) server.start() for cfg in config: wait_until_connectable(host=cfg.host, port=cfg.port, timeout=10) yield None server.terminate() @pytest.fixture(scope='session', autouse=True) def web_server(target_ssl_certfile, target_ssl_keyfile): config = [ HttpServerConfig(host=TEST_HOST_IPV4, port=TEST_PORT_IPV4), HttpServerConfig( host=TEST_HOST_IPV4, port=TEST_PORT_IPV4_HTTPS, certfile=target_ssl_certfile, keyfile=target_ssl_keyfile, ), ] if not SKIP_IPV6_TESTS: config.append(HttpServerConfig(host=TEST_HOST_IPV6, port=TEST_PORT_IPV6)) server = HttpServer(config=config) server.start() for cfg in config: server.wait_until_connectable(host=cfg.host, port=cfg.port) yield None server.terminate() aiohttp-socks-0.8.4/tests/http_app.py000066400000000000000000000012571450674506600176700ustar00rootroot00000000000000import ssl import time import flask # noqa from flask import request # noqa app = flask.Flask(__name__) @app.route('/ip') def ip(): return request.remote_addr @app.route('/delay/') def delay(seconds): time.sleep(seconds) return 'ok' def run_app(host: str, port: int, certfile: str = None, keyfile: str = None): if certfile and keyfile: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) ssl_context.load_cert_chain(certfile, keyfile) else: ssl_context = None print('Starting http server on {}:{}...'.format(host, port)) app.run(debug=False, host=host, port=port, threaded=True, ssl_context=ssl_context) aiohttp-socks-0.8.4/tests/http_server.py000066400000000000000000000024351450674506600204150ustar00rootroot00000000000000import typing import time from multiprocessing import Process from tests.utils import is_connectable from tests.http_app import run_app class HttpServerConfig(typing.NamedTuple): host: str port: int certfile: str = None keyfile: str = None def to_dict(self): d = {} for key, val in self._asdict().items(): if val is not None: d[key] = val return d class HttpServer: def __init__(self, config: typing.Iterable[HttpServerConfig]): self.config = config self.workers = [] def start(self): for cfg in self.config: p = Process(target=run_app, kwargs=cfg.to_dict()) self.workers.append(p) for p in self.workers: p.start() def terminate(self): for p in self.workers: p.terminate() def wait_until_connectable(self, host, port, timeout=10): count = 0 while not is_connectable(host=host, port=port): if count >= timeout: self.terminate() raise Exception( 'The http server has not available ' 'by (%s, %s) in %d seconds' % (host, port, timeout)) count += 1 time.sleep(1) return True aiohttp-socks-0.8.4/tests/mocks.py000066400000000000000000000043011450674506600171560ustar00rootroot00000000000000import socket from tests.config import ( TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4, TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6, ) def getaddrinfo_sync_mock(): _orig_getaddrinfo = socket.getaddrinfo def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.0.0.1', port))] if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): return [(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', port, 0, 0))] return _orig_getaddrinfo(host, port, family, type, proto, flags) return getaddrinfo def getaddrinfo_async_mock(origin_getaddrinfo): async def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.0.0.1', port))] if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): return [(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', port, 0, 0))] return await origin_getaddrinfo( host, port, family=family, type=type, proto=proto, flags=flags, ) return getaddrinfo def _resolve_local(host): if host in (TEST_HOST_NAME_IPV4, PROXY_HOST_NAME_IPV4): return socket.AF_INET, '127.0.0.1' if host in (TEST_HOST_NAME_IPV6, PROXY_HOST_NAME_IPV6): return socket.AF_INET6, '::1' return None def sync_resolve_factory(cls): original_resolver = cls.resolve def new_resolver(self, host, port=0, family=socket.AF_UNSPEC): res = _resolve_local(host) if res is not None: return res return original_resolver(self, host=host, port=port, family=family) return new_resolver def async_resolve_factory(cls): original_resolver = cls.resolve async def new_resolver(self, host, port=0, family=socket.AF_UNSPEC): res = _resolve_local(host) if res is not None: return res return await original_resolver(self, host=host, port=port, family=family) return new_resolver aiohttp-socks-0.8.4/tests/proxy_server.py000066400000000000000000000064501450674506600206200ustar00rootroot00000000000000import ssl import typing from multiprocessing import Process from unittest import mock import anyio from anyio import create_tcp_listener from anyio.streams.tls import TLSListener from tiny_proxy import ( HttpProxyHandler, Socks5ProxyHandler, Socks4ProxyHandler, HttpProxy, Socks4Proxy, Socks5Proxy, AbstractProxy, ) from tests.mocks import getaddrinfo_async_mock class ProxyConfig(typing.NamedTuple): proxy_type: str host: str port: int username: typing.Optional[str] = None password: typing.Optional[str] = None ssl_certfile: typing.Optional[str] = None ssl_keyfile: typing.Optional[str] = None def to_dict(self): d = {} for key, val in self._asdict().items(): if val is not None: d[key] = val return d cls_map = { 'http': HttpProxyHandler, 'socks4': Socks4ProxyHandler, 'socks5': Socks5ProxyHandler, } def connect_to_remote_factory(cls: typing.Type[AbstractProxy]): """ simulate target host connection timeout """ origin_connect_to_remote = cls.connect_to_remote async def new_connect_to_remote(self): await anyio.sleep(0.01) return await origin_connect_to_remote(self) return new_connect_to_remote @mock.patch.object( HttpProxy, attribute='connect_to_remote', new=connect_to_remote_factory(HttpProxy), ) @mock.patch.object( Socks4Proxy, attribute='connect_to_remote', new=connect_to_remote_factory(Socks4Proxy), ) @mock.patch.object( Socks5Proxy, attribute='connect_to_remote', new=connect_to_remote_factory(Socks5Proxy), ) @mock.patch('anyio._core._sockets.getaddrinfo', new=getaddrinfo_async_mock(anyio.getaddrinfo)) def start( proxy_type, host, port, ssl_certfile=None, ssl_keyfile=None, **kwargs, ): handler_cls = cls_map.get(proxy_type) if not handler_cls: raise RuntimeError(f'Unsupported type: {proxy_type}') if ssl_certfile and ssl_keyfile: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile) else: ssl_context = None print(f'Starting {proxy_type} proxy on {host}:{port}...') handler = handler_cls(**kwargs) async def serve(): listener = await create_tcp_listener(local_host=host, local_port=port) if ssl_context is not None: listener = TLSListener(listener=listener, ssl_context=ssl_context) async with listener: await listener.serve(handler.handle) anyio.run(serve) class ProxyServer: workers: typing.List[Process] def __init__(self, config: typing.Iterable[ProxyConfig]): self.config = config self.workers = [] def start(self): for cfg in self.config: print( 'Starting {} proxy on {}:{}; certfile={}, keyfile={}...'.format( cfg.proxy_type, cfg.host, cfg.port, cfg.ssl_certfile, cfg.ssl_keyfile, ) ) p = Process(target=start, kwargs=cfg.to_dict(), daemon=True) self.workers.append(p) for p in self.workers: p.start() def terminate(self): for p in self.workers: p.terminate() aiohttp-socks-0.8.4/tests/test_connector.py000066400000000000000000000174021450674506600211010ustar00rootroot00000000000000import asyncio import ssl import aiohttp import pytest # noqa from aiohttp import ClientResponse, TCPConnector from yarl import URL # noqa from aiohttp_socks import ( ProxyType, ProxyConnector, ChainProxyConnector, ProxyInfo, ProxyError, ProxyConnectionError, ProxyTimeoutError, open_connection, create_connection, ) from tests.config import ( TEST_URL_IPV4, SOCKS5_IPV4_URL, PROXY_HOST_IPV4, SOCKS5_PROXY_PORT, LOGIN, PASSWORD, TEST_URL_IPV4_DELAY, SKIP_IPV6_TESTS, SOCKS5_IPV6_URL, SOCKS4_URL, HTTP_PROXY_URL, SOCKS4_PROXY_PORT, HTTP_PROXY_PORT, TEST_URL_IPV4_HTTPS, ) async def fetch( connector: TCPConnector, url: str, timeout=None, ssl_context=None, ) -> ClientResponse: url = URL(url) if url.scheme == 'https': dest_ssl = ssl_context else: dest_ssl = None async with aiohttp.ClientSession(connector=connector) as session: async with session.get(url, ssl=dest_ssl, timeout=timeout) as resp: return resp @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.parametrize('rdns', (True, False)) @pytest.mark.asyncio async def test_socks5_proxy_ipv4(url, rdns, target_ssl_context): connector = ProxyConnector.from_url(SOCKS5_IPV4_URL, rdns=rdns) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.asyncio async def test_socks5_proxy_with_invalid_credentials(target_ssl_context): connector = ProxyConnector( proxy_type=ProxyType.SOCKS5, host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT, username=LOGIN, password=PASSWORD + 'aaa', ) with pytest.raises(ProxyError): await fetch( connector=connector, url=TEST_URL_IPV4, ssl_context=target_ssl_context, ) @pytest.mark.asyncio async def test_socks5_proxy_with_timeout(target_ssl_context): connector = ProxyConnector( proxy_type=ProxyType.SOCKS5, host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT, username=LOGIN, password=PASSWORD, ) with pytest.raises(asyncio.TimeoutError): await fetch( connector=connector, url=TEST_URL_IPV4_DELAY, timeout=1, ssl_context=target_ssl_context, ) @pytest.mark.asyncio async def test_socks5_proxy_with_proxy_connect_timeout(target_ssl_context): connector = ProxyConnector.from_url(SOCKS5_IPV4_URL) timeout = aiohttp.ClientTimeout(total=32, sock_connect=0.001) with pytest.raises(ProxyTimeoutError): await fetch( connector=connector, url=TEST_URL_IPV4, timeout=timeout, ssl_context=target_ssl_context, ) @pytest.mark.asyncio async def test_socks5_proxy_with_invalid_proxy_port(unused_tcp_port, target_ssl_context): connector = ProxyConnector( proxy_type=ProxyType.SOCKS5, host=PROXY_HOST_IPV4, port=unused_tcp_port, username=LOGIN, password=PASSWORD, ) with pytest.raises(ProxyConnectionError): await fetch( connector=connector, url=TEST_URL_IPV4, ssl_context=target_ssl_context, ) @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.skipif(SKIP_IPV6_TESTS, reason="TravisCI doesn't support ipv6") @pytest.mark.asyncio async def test_socks5_proxy_ipv6(url, target_ssl_context): connector = ProxyConnector.from_url(SOCKS5_IPV6_URL) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.parametrize('rdns', (True, False)) @pytest.mark.asyncio async def test_socks4_proxy(url, rdns, target_ssl_context): connector = ProxyConnector.from_url( SOCKS4_URL, rdns=rdns, ) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.asyncio async def test_http_proxy(url, target_ssl_context): connector = ProxyConnector.from_url(HTTP_PROXY_URL) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.asyncio async def test_chain_proxy_from_url(url, target_ssl_context): connector = ChainProxyConnector.from_urls([SOCKS5_IPV4_URL, SOCKS4_URL, HTTP_PROXY_URL]) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.parametrize('rdns', (True, False)) @pytest.mark.asyncio async def test_chain_proxy_ctor(url, rdns, target_ssl_context): connector = ChainProxyConnector( [ ProxyInfo( proxy_type=ProxyType.SOCKS5, host=PROXY_HOST_IPV4, port=SOCKS5_PROXY_PORT, username=LOGIN, password=PASSWORD, rdns=rdns, ), ProxyInfo( proxy_type=ProxyType.SOCKS4, host=PROXY_HOST_IPV4, port=SOCKS4_PROXY_PORT, username=LOGIN, rdns=rdns, ), ProxyInfo( proxy_type=ProxyType.HTTP, host=PROXY_HOST_IPV4, port=HTTP_PROXY_PORT, username=LOGIN, password=PASSWORD, ), ] ) res = await fetch( connector=connector, url=url, ssl_context=target_ssl_context, ) assert res.status == 200 @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.parametrize('rdns', (True, False)) @pytest.mark.asyncio async def test_socks5_open_connection(url, rdns, target_ssl_context): url = URL(url) ssl_context = None if url.scheme == 'https': ssl_context = target_ssl_context reader, writer = await open_connection( proxy_url=SOCKS5_IPV4_URL, host=url.host, port=url.port, ssl=ssl_context, server_hostname=url.host if ssl_context else None, rdns=rdns, ) request = "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % (url.path_qs, url.host) writer.write(request.encode()) response = await reader.read(-1) assert b'200 OK' in response @pytest.mark.parametrize('url', (TEST_URL_IPV4, TEST_URL_IPV4_HTTPS)) @pytest.mark.parametrize('rdns', (True, False)) @pytest.mark.asyncio async def test_socks5_http_create_connection( url: str, rdns: bool, event_loop: asyncio.AbstractEventLoop, target_ssl_context: ssl.SSLContext, ): url = URL(url) ssl_context = None if url.scheme == 'https': ssl_context = target_ssl_context reader = asyncio.StreamReader(loop=event_loop) protocol = asyncio.StreamReaderProtocol(reader, loop=event_loop) transport, _ = await create_connection( proxy_url=SOCKS5_IPV4_URL, protocol_factory=lambda: protocol, host=url.host, port=url.port, ssl=ssl_context, server_hostname=url.host if ssl_context else None, rdns=rdns, ) writer = asyncio.StreamWriter(transport, protocol, reader, event_loop) request = "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % (url.path_qs, url.host) writer.write(request.encode()) response = await reader.read(-1) assert b'200 OK' in response aiohttp-socks-0.8.4/tests/utils.py000066400000000000000000000011211450674506600171770ustar00rootroot00000000000000import socket import time def is_connectable(host, port): try: sock = socket.create_connection((host, port), 1) except socket.error: return False else: sock.close() return True def wait_until_connectable(host, port, timeout=10): count = 0 while not is_connectable(host=host, port=port): if count >= timeout: raise Exception( f'The proxy server has not available by ({host}, {port}) in {timeout:d} seconds' ) count += 1 time.sleep(1) return True