├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.rst ├── aiosocks ├── __init__.py ├── connector.py ├── constants.py ├── errors.py ├── helpers.py ├── protocols.py └── test_utils.py ├── setup.py └── tests ├── conftest.py ├── sample.crt ├── sample.key ├── test_connector.py ├── test_create_connect.py ├── test_functional.py ├── test_helpers.py └── test_protocols.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = aiosocks, tests 4 | omit = site-packages,aiosocks/test_utils.py 5 | 6 | [html] 7 | directory = coverage -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: no 2 | language: python 3 | 4 | python: 5 | - 3.5 6 | - 3.6 7 | - 3.7 8 | - 3.8 9 | 10 | os: 11 | - linux 12 | 13 | cache: 14 | directories: 15 | - $HOME/.cache/pip 16 | 17 | before_cache: 18 | - rm -f $HOME/.cache/pip/log/debug.log 19 | 20 | install: 21 | - pip install --upgrade pip wheel 22 | - pip install --upgrade setuptools 23 | - pip install pip 24 | - pip install flake8 25 | - pip install pyflakes 26 | - pip install coverage 27 | - pip install pytest 28 | - pip install pytest-cov 29 | - pip install aiodns 30 | - pip install aiohttp 31 | - pip install coveralls 32 | 33 | script: 34 | - cd $TRAVIS_BUILD_DIR 35 | - flake8 aiosocks tests 36 | - python setup.py develop && py.test --cov=aiosocks tests 37 | - python setup.py check -rm 38 | - if python -c "import sys; sys.exit(sys.version_info < (3,5))"; then 39 | python setup.py check -s; 40 | fi 41 | 42 | after_success: 43 | coveralls -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SOCKS proxy client for asyncio and aiohttp 2 | ========================================== 3 | .. image:: https://travis-ci.org/nibrag/aiosocks.svg?branch=master 4 | :target: https://travis-ci.org/nibrag/aiosocks 5 | :align: right 6 | 7 | .. image:: https://coveralls.io/repos/github/nibrag/aiosocks/badge.svg?branch=master 8 | :target: https://coveralls.io/github/nibrag/aiosocks?branch=master 9 | :align: right 10 | 11 | .. image:: https://badge.fury.io/py/aiosocks.svg 12 | :target: https://badge.fury.io/py/aiosocks 13 | 14 | 15 | Dependencies 16 | ------------ 17 | python 3.5+ 18 | aiohttp 2.3.2+ 19 | 20 | Features 21 | -------- 22 | - SOCKS4, SOCKS4a and SOCKS5 version 23 | - ProxyConnector for aiohttp 24 | - SOCKS "CONNECT" command 25 | 26 | TODO 27 | ---- 28 | - UDP associate 29 | - TCP port binding 30 | 31 | Installation 32 | ------------ 33 | You can install it using Pip: 34 | 35 | .. code-block:: 36 | 37 | pip install aiosocks 38 | 39 | If you want the latest development version, you can install it from source: 40 | 41 | .. code-block:: 42 | 43 | git clone git@github.com:nibrag/aiosocks.git 44 | cd aiosocks 45 | python setup.py install 46 | 47 | Usage 48 | ----- 49 | direct usage 50 | ^^^^^^^^^^^^ 51 | 52 | .. code-block:: python 53 | 54 | import asyncio 55 | import aiosocks 56 | 57 | 58 | async def connect(): 59 | socks5_addr = aiosocks.Socks5Addr('127.0.0.1', 1080) 60 | socks4_addr = aiosocks.Socks4Addr('127.0.0.1', 1080) 61 | 62 | socks5_auth = aiosocks.Socks5Auth('login', 'pwd') 63 | socks4_auth = aiosocks.Socks4Auth('ident') 64 | 65 | dst = ('github.com', 80) 66 | 67 | # socks5 connect 68 | transport, protocol = await aiosocks.create_connection( 69 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 70 | 71 | # socks4 connect 72 | transport, protocol = await aiosocks.create_connection( 73 | lambda: Protocol, proxy=socks4_addr, proxy_auth=socks4_auth, dst=dst) 74 | 75 | # socks4 without auth and local domain name resolving 76 | transport, protocol = await aiosocks.create_connection( 77 | lambda: Protocol, proxy=socks4_addr, proxy_auth=None, dst=dst, remote_resolve=False) 78 | 79 | # use socks protocol 80 | transport, protocol = await aiosocks.create_connection( 81 | None, proxy=socks4_addr, proxy_auth=None, dst=dst) 82 | 83 | if __name__ == '__main__': 84 | loop = asyncio.get_event_loop() 85 | loop.run_until_complete(connect()) 86 | loop.close() 87 | 88 | 89 | **A wrapper for create_connection() returning a (reader, writer) pair** 90 | 91 | .. code-block:: python 92 | 93 | # StreamReader, StreamWriter 94 | reader, writer = await aiosocks.open_connection( 95 | proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst, remote_resolve=True) 96 | 97 | data = await reader.read(10) 98 | writer.write('data') 99 | 100 | error handling 101 | ^^^^^^^^^^^^^^ 102 | 103 | `SocksError` is a base class for: 104 | - `NoAcceptableAuthMethods` 105 | - `LoginAuthenticationFailed` 106 | - `InvalidServerVersion` 107 | - `InvalidServerReply` 108 | 109 | .. code-block:: python 110 | 111 | try: 112 | transport, protocol = await aiosocks.create_connection( 113 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 114 | except aiosocks.SocksConnectionError: 115 | # connection error 116 | except aiosocks.LoginAuthenticationFailed: 117 | # auth failed 118 | except aiosocks.NoAcceptableAuthMethods: 119 | # All offered SOCKS5 authentication methods were rejected 120 | except (aiosocks.InvalidServerVersion, aiosocks.InvalidServerReply): 121 | # something wrong 122 | except aiosocks.SocksError: 123 | # something other 124 | 125 | or 126 | 127 | .. code-block:: python 128 | 129 | try: 130 | transport, protocol = await aiosocks.create_connection( 131 | lambda: Protocol, proxy=socks5_addr, proxy_auth=socks5_auth, dst=dst) 132 | except aiosocks.SocksConnectionError: 133 | # connection error 134 | except aiosocks.SocksError: 135 | # socks error 136 | 137 | aiohttp usage 138 | ^^^^^^^^^^^^^ 139 | 140 | .. code-block:: python 141 | 142 | import asyncio 143 | import aiohttp 144 | import aiosocks 145 | from aiosocks.connector import ProxyConnector, ProxyClientRequest 146 | 147 | 148 | async def load_github_main(): 149 | auth5 = aiosocks.Socks5Auth('proxyuser1', password='pwd') 150 | auth4 = aiosocks.Socks4Auth('proxyuser1') 151 | ba = aiohttp.BasicAuth('login') 152 | 153 | # remote resolve 154 | conn = ProxyConnector(remote_resolve=True) 155 | 156 | # or locale resolve 157 | conn = ProxyConnector(remote_resolve=False) 158 | 159 | try: 160 | with aiohttp.ClientSession(connector=conn, request_class=ProxyClientRequest) as session: 161 | # socks5 proxy 162 | async with session.get('http://github.com/', proxy='socks5://127.0.0.1:1080', 163 | proxy_auth=auth5) as resp: 164 | if resp.status == 200: 165 | print(await resp.text()) 166 | 167 | # socks4 proxy 168 | async with session.get('http://github.com/', proxy='socks4://127.0.0.1:1081', 169 | proxy_auth=auth4) as resp: 170 | if resp.status == 200: 171 | print(await resp.text()) 172 | 173 | # http proxy 174 | async with session.get('http://github.com/', proxy='http://127.0.0.1:8080', 175 | proxy_auth=ba) as resp: 176 | if resp.status == 200: 177 | print(await resp.text()) 178 | except aiohttp.ClientProxyConnectionError: 179 | # connection problem 180 | except aiohttp.ClientConnectorError: 181 | # ssl error, certificate error, etc 182 | except aiosocks.SocksError: 183 | # communication problem 184 | 185 | 186 | if __name__ == '__main__': 187 | loop = asyncio.get_event_loop() 188 | loop.run_until_complete(load_github_main()) 189 | loop.close() 190 | -------------------------------------------------------------------------------- /aiosocks/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from .errors import ( 3 | SocksError, NoAcceptableAuthMethods, LoginAuthenticationFailed, 4 | SocksConnectionError, InvalidServerReply, InvalidServerVersion 5 | ) 6 | from .helpers import ( 7 | SocksAddr, Socks4Addr, Socks5Addr, Socks4Auth, Socks5Auth 8 | ) 9 | from .protocols import Socks4Protocol, Socks5Protocol, DEFAULT_LIMIT 10 | 11 | __version__ = '0.2.6' 12 | 13 | __all__ = ('Socks4Protocol', 'Socks5Protocol', 'Socks4Auth', 14 | 'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksError', 15 | 'NoAcceptableAuthMethods', 'LoginAuthenticationFailed', 16 | 'SocksConnectionError', 'InvalidServerVersion', 17 | 'InvalidServerReply', 'create_connection', 'open_connection') 18 | 19 | 20 | async def create_connection(protocol_factory, proxy, proxy_auth, dst, *, 21 | remote_resolve=True, loop=None, ssl=None, family=0, 22 | proto=0, flags=0, sock=None, local_addr=None, 23 | server_hostname=None, reader_limit=DEFAULT_LIMIT): 24 | assert isinstance(proxy, SocksAddr), ( 25 | 'proxy must be Socks4Addr() or Socks5Addr() tuple' 26 | ) 27 | 28 | assert proxy_auth is None or isinstance(proxy_auth, 29 | (Socks4Auth, Socks5Auth)), ( 30 | 'proxy_auth must be None or Socks4Auth() ' 31 | 'or Socks5Auth() tuple', proxy_auth 32 | ) 33 | assert isinstance(dst, (tuple, list)) and len(dst) == 2, ( 34 | 'invalid dst format, tuple("dst_host", dst_port))' 35 | ) 36 | 37 | if (isinstance(proxy, Socks4Addr) and not 38 | (proxy_auth is None or isinstance(proxy_auth, Socks4Auth))): 39 | raise ValueError( 40 | "proxy is Socks4Addr but proxy_auth is not Socks4Auth" 41 | ) 42 | 43 | if (isinstance(proxy, Socks5Addr) and not 44 | (proxy_auth is None or isinstance(proxy_auth, Socks5Auth))): 45 | raise ValueError( 46 | "proxy is Socks5Addr but proxy_auth is not Socks5Auth" 47 | ) 48 | 49 | if server_hostname is not None and not ssl: 50 | raise ValueError('server_hostname is only meaningful with ssl') 51 | 52 | if server_hostname is None and ssl: 53 | # read details: asyncio.create_connection 54 | server_hostname = dst[0] 55 | 56 | loop = loop or asyncio.get_event_loop() 57 | waiter = asyncio.Future(loop=loop) 58 | 59 | def socks_factory(): 60 | if isinstance(proxy, Socks4Addr): 61 | socks_proto = Socks4Protocol 62 | else: 63 | socks_proto = Socks5Protocol 64 | 65 | return socks_proto(proxy=proxy, proxy_auth=proxy_auth, dst=dst, 66 | app_protocol_factory=protocol_factory, 67 | waiter=waiter, remote_resolve=remote_resolve, 68 | loop=loop, ssl=ssl, server_hostname=server_hostname, 69 | reader_limit=reader_limit) 70 | 71 | try: 72 | transport, protocol = await loop.create_connection( 73 | socks_factory, proxy.host, proxy.port, family=family, 74 | proto=proto, flags=flags, sock=sock, local_addr=local_addr) 75 | except OSError as exc: 76 | raise SocksConnectionError( 77 | '[Errno %s] Can not connect to proxy %s:%d [%s]' % 78 | (exc.errno, proxy.host, proxy.port, exc.strerror)) from exc 79 | 80 | try: 81 | await waiter 82 | except: # noqa 83 | transport.close() 84 | raise 85 | 86 | return protocol.app_transport, protocol.app_protocol 87 | 88 | 89 | async def open_connection(proxy, proxy_auth, dst, *, remote_resolve=True, 90 | loop=None, limit=DEFAULT_LIMIT, **kwds): 91 | _, protocol = await create_connection( 92 | None, proxy, proxy_auth, dst, reader_limit=limit, 93 | remote_resolve=remote_resolve, loop=loop, **kwds) 94 | 95 | return protocol.reader, protocol.writer 96 | -------------------------------------------------------------------------------- /aiosocks/connector.py: -------------------------------------------------------------------------------- 1 | try: 2 | import aiohttp 3 | from aiohttp.client_exceptions import cert_errors, ssl_errors 4 | except ImportError: # pragma: no cover 5 | raise ImportError('aiosocks.SocksConnector require aiohttp library') 6 | 7 | from .errors import SocksConnectionError 8 | from .helpers import Socks4Auth, Socks5Auth, Socks4Addr, Socks5Addr 9 | from . import create_connection 10 | 11 | __all__ = ('ProxyConnector', 'ProxyClientRequest') 12 | 13 | 14 | class ProxyClientRequest(aiohttp.ClientRequest): 15 | def update_proxy(self, proxy, proxy_auth, proxy_headers): 16 | if proxy and proxy.scheme not in ['http', 'socks4', 'socks5']: 17 | raise ValueError( 18 | "Only http, socks4 and socks5 proxies are supported") 19 | if proxy and proxy_auth: 20 | if proxy.scheme == 'http' and \ 21 | not isinstance(proxy_auth, aiohttp.BasicAuth): 22 | raise ValueError("proxy_auth must be None or " 23 | "BasicAuth() tuple for http proxy") 24 | if proxy.scheme == 'socks4' and \ 25 | not isinstance(proxy_auth, Socks4Auth): 26 | raise ValueError("proxy_auth must be None or Socks4Auth() " 27 | "tuple for socks4 proxy") 28 | if proxy.scheme == 'socks5' and \ 29 | not isinstance(proxy_auth, Socks5Auth): 30 | raise ValueError("proxy_auth must be None or Socks5Auth() " 31 | "tuple for socks5 proxy") 32 | self.proxy = proxy 33 | self.proxy_auth = proxy_auth 34 | self.proxy_headers = proxy_headers 35 | 36 | 37 | class ProxyConnector(aiohttp.TCPConnector): 38 | def __init__(self, remote_resolve=True, **kwargs): 39 | super().__init__(**kwargs) 40 | 41 | self._remote_resolve = remote_resolve 42 | 43 | async def _create_proxy_connection(self, req, *args, **kwargs): 44 | if req.proxy.scheme == 'http': 45 | return await super()._create_proxy_connection(req, *args, **kwargs) 46 | else: 47 | return await self._create_socks_connection(req) 48 | 49 | async def _wrap_create_socks_connection(self, *args, req, **kwargs): 50 | try: 51 | return await create_connection(*args, **kwargs) 52 | except cert_errors as exc: 53 | raise aiohttp.ClientConnectorCertificateError( 54 | req.connection_key, exc) from exc 55 | except ssl_errors as exc: 56 | raise aiohttp.ClientConnectorSSLError( 57 | req.connection_key, exc) from exc 58 | except (OSError, SocksConnectionError) as exc: 59 | raise aiohttp.ClientProxyConnectionError( 60 | req.connection_key, exc) from exc 61 | 62 | def _get_fingerprint_and_hashfunc(self, req): 63 | base = super() 64 | if hasattr(base, '_get_fingerprint_and_hashfunc'): 65 | return base._get_fingerprint_and_hashfunc(req) 66 | 67 | fingerprint = self._get_fingerprint(req) 68 | if fingerprint: 69 | return (fingerprint.fingerprint, fingerprint._hashfunc) 70 | 71 | return (None, None) 72 | 73 | async def _create_socks_connection(self, req): 74 | sslcontext = self._get_ssl_context(req) 75 | fingerprint, hashfunc = self._get_fingerprint_and_hashfunc(req) 76 | 77 | if not self._remote_resolve: 78 | try: 79 | dst_hosts = list(await self._resolve_host(req.host, req.port)) 80 | dst = dst_hosts[0]['host'], dst_hosts[0]['port'] 81 | except OSError as exc: 82 | raise aiohttp.ClientConnectorError( 83 | req.connection_key, exc) from exc 84 | else: 85 | dst = req.host, req.port 86 | 87 | try: 88 | proxy_hosts = await self._resolve_host( 89 | req.proxy.host, req.proxy.port) 90 | except OSError as exc: 91 | raise aiohttp.ClientConnectorError( 92 | req.connection_key, exc) from exc 93 | 94 | last_exc = None 95 | 96 | for hinfo in proxy_hosts: 97 | if req.proxy.scheme == 'socks4': 98 | proxy = Socks4Addr(hinfo['host'], hinfo['port']) 99 | else: 100 | proxy = Socks5Addr(hinfo['host'], hinfo['port']) 101 | 102 | try: 103 | transp, proto = await self._wrap_create_socks_connection( 104 | self._factory, proxy, req.proxy_auth, dst, 105 | loop=self._loop, remote_resolve=self._remote_resolve, 106 | ssl=sslcontext, family=hinfo['family'], 107 | proto=hinfo['proto'], flags=hinfo['flags'], 108 | local_addr=self._local_addr, req=req, 109 | server_hostname=req.host if sslcontext else None) 110 | except aiohttp.ClientConnectorError as exc: 111 | last_exc = exc 112 | continue 113 | 114 | has_cert = transp.get_extra_info('sslcontext') 115 | if has_cert and fingerprint: 116 | sock = transp.get_extra_info('socket') 117 | if not hasattr(sock, 'getpeercert'): 118 | # Workaround for asyncio 3.5.0 119 | # Starting from 3.5.1 version 120 | # there is 'ssl_object' extra info in transport 121 | sock = transp._ssl_protocol._sslpipe.ssl_object 122 | # gives DER-encoded cert as a sequence of bytes (or None) 123 | cert = sock.getpeercert(binary_form=True) 124 | assert cert 125 | got = hashfunc(cert).digest() 126 | expected = fingerprint 127 | if got != expected: 128 | transp.close() 129 | if not self._cleanup_closed_disabled: 130 | self._cleanup_closed_transports.append(transp) 131 | last_exc = aiohttp.ServerFingerprintMismatch( 132 | expected, got, req.host, req.port) 133 | continue 134 | return transp, proto 135 | else: 136 | raise last_exc 137 | -------------------------------------------------------------------------------- /aiosocks/constants.py: -------------------------------------------------------------------------------- 1 | RSV = NULL = 0x00 2 | SOCKS_VER4 = 0x04 3 | SOCKS_VER5 = 0x05 4 | 5 | SOCKS_CMD_CONNECT = 0x01 6 | SOCKS_CMD_BIND = 0x02 7 | SOCKS_CMD_UDP_ASSOCIATE = 0x03 8 | SOCKS4_GRANTED = 0x5A 9 | SOCKS5_GRANTED = 0x00 10 | 11 | SOCKS5_AUTH_ANONYMOUS = 0x00 12 | SOCKS5_AUTH_UNAME_PWD = 0x02 13 | SOCKS5_AUTH_NO_ACCEPTABLE_METHODS = 0xFF 14 | 15 | SOCKS5_ATYP_IPv4 = 0x01 16 | SOCKS5_ATYP_DOMAIN = 0x03 17 | SOCKS5_ATYP_IPv6 = 0x04 18 | 19 | SOCKS4_ERRORS = { 20 | 0x5B: 'Request rejected or failed', 21 | 0x5C: 'Request rejected because SOCKS server ' 22 | 'cannot connect to identd on the client', 23 | 0x5D: 'Request rejected because the client program ' 24 | 'and identd report different user-ids' 25 | } 26 | 27 | SOCKS5_ERRORS = { 28 | 0x01: 'General SOCKS server failure', 29 | 0x02: 'Connection not allowed by ruleset', 30 | 0x03: 'Network unreachable', 31 | 0x04: 'Host unreachable', 32 | 0x05: 'Connection refused', 33 | 0x06: 'TTL expired', 34 | 0x07: 'Command not supported, or protocol error', 35 | 0x08: 'Address type not supported' 36 | } 37 | -------------------------------------------------------------------------------- /aiosocks/errors.py: -------------------------------------------------------------------------------- 1 | class SocksError(Exception): 2 | pass 3 | 4 | 5 | class NoAcceptableAuthMethods(SocksError): 6 | pass 7 | 8 | 9 | class LoginAuthenticationFailed(SocksError): 10 | pass 11 | 12 | 13 | class InvalidServerVersion(SocksError): 14 | pass 15 | 16 | 17 | class InvalidServerReply(SocksError): 18 | pass 19 | 20 | 21 | class SocksConnectionError(OSError): 22 | pass 23 | -------------------------------------------------------------------------------- /aiosocks/helpers.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | __all__ = ('Socks4Auth', 'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksAddr') 4 | 5 | 6 | class Socks4Auth(namedtuple('Socks4Auth', ['login', 'encoding'])): 7 | def __new__(cls, login, encoding='utf-8'): 8 | if login is None: 9 | raise ValueError('None is not allowed as login value') 10 | 11 | return super().__new__(cls, login.encode(encoding), encoding) 12 | 13 | 14 | class Socks5Auth(namedtuple('Socks5Auth', ['login', 'password', 'encoding'])): 15 | def __new__(cls, login, password, encoding='utf-8'): 16 | if login is None: 17 | raise ValueError('None is not allowed as login value') 18 | 19 | if password is None: 20 | raise ValueError('None is not allowed as password value') 21 | 22 | return super().__new__(cls, 23 | login.encode(encoding), 24 | password.encode(encoding), encoding) 25 | 26 | 27 | class SocksAddr(namedtuple('SocksServer', ['host', 'port'])): 28 | def __new__(cls, host, port=1080): 29 | if host is None: 30 | raise ValueError('None is not allowed as host value') 31 | 32 | if port is None: 33 | port = 1080 # default socks server port 34 | 35 | return super().__new__(cls, host, port) 36 | 37 | 38 | class Socks4Addr(SocksAddr): 39 | pass 40 | 41 | 42 | class Socks5Addr(SocksAddr): 43 | pass 44 | -------------------------------------------------------------------------------- /aiosocks/protocols.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket 3 | import struct 4 | from asyncio import sslproto 5 | 6 | from . import constants as c 7 | from .helpers import ( 8 | Socks4Addr, Socks5Addr, Socks5Auth, Socks4Auth 9 | ) 10 | from .errors import ( 11 | SocksError, NoAcceptableAuthMethods, LoginAuthenticationFailed, 12 | InvalidServerReply, InvalidServerVersion 13 | ) 14 | 15 | 16 | DEFAULT_LIMIT = getattr(asyncio.streams, '_DEFAULT_LIMIT', 2**16) 17 | 18 | 19 | class BaseSocksProtocol(asyncio.StreamReaderProtocol): 20 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, *, 21 | remote_resolve=True, loop=None, ssl=False, 22 | server_hostname=None, negotiate_done_cb=None, 23 | reader_limit=DEFAULT_LIMIT): 24 | if not isinstance(dst, (tuple, list)) or len(dst) != 2: 25 | raise ValueError( 26 | 'Invalid dst format, tuple("dst_host", dst_port))' 27 | ) 28 | 29 | self._proxy = proxy 30 | self._auth = proxy_auth 31 | self._dst_host, self._dst_port = dst 32 | self._remote_resolve = remote_resolve 33 | self._waiter = waiter 34 | self._ssl = ssl 35 | self._server_hostname = server_hostname 36 | self._negotiate_done_cb = negotiate_done_cb 37 | self._loop = loop or asyncio.get_event_loop() 38 | 39 | self._transport = None 40 | self._negotiate_done = False 41 | self._proxy_peername = None 42 | self._proxy_sockname = None 43 | 44 | if app_protocol_factory: 45 | self._app_protocol = app_protocol_factory() 46 | else: 47 | self._app_protocol = self 48 | 49 | reader = asyncio.StreamReader(loop=self._loop, limit=reader_limit) 50 | 51 | super().__init__(stream_reader=reader, 52 | client_connected_cb=self.negotiate, loop=self._loop) 53 | 54 | async def negotiate(self, reader, writer): 55 | try: 56 | req = self.socks_request(c.SOCKS_CMD_CONNECT) 57 | self._proxy_peername, self._proxy_sockname = await req 58 | except SocksError as exc: 59 | exc = SocksError('Can not connect to %s:%s. %s' % 60 | (self._dst_host, self._dst_port, exc)) 61 | if not self._waiter.cancelled(): 62 | self._loop.call_soon(self._waiter.set_exception, exc) 63 | except Exception as exc: 64 | if not self._waiter.cancelled(): 65 | self._loop.call_soon(self._waiter.set_exception, exc) 66 | else: 67 | self._negotiate_done = True 68 | 69 | if self._ssl: 70 | # Creating a ssl transport needs to be reworked. 71 | # See details: http://bugs.python.org/issue23749 72 | self._tls_protocol = sslproto.SSLProtocol( 73 | app_protocol=self, sslcontext=self._ssl, server_side=False, 74 | server_hostname=self._server_hostname, waiter=self._waiter, 75 | loop=self._loop, call_connection_made=False) 76 | 77 | # starttls 78 | original_transport = self._transport 79 | self._transport.set_protocol(self._tls_protocol) 80 | self._transport = self._tls_protocol._app_transport 81 | 82 | self._tls_protocol.connection_made(original_transport) 83 | 84 | self._loop.call_soon(self._app_protocol.connection_made, 85 | self._transport) 86 | else: 87 | self._loop.call_soon(self._app_protocol.connection_made, 88 | self._transport) 89 | self._loop.call_soon(self._waiter.set_result, True) 90 | 91 | if self._negotiate_done_cb is not None: 92 | res = self._negotiate_done_cb(reader, writer) 93 | 94 | if asyncio.iscoroutine(res): 95 | self._loop.create_task(res) 96 | return res 97 | 98 | def connection_made(self, transport): 99 | # connection_made is called 100 | if self._transport: 101 | return 102 | 103 | super().connection_made(transport) 104 | self._transport = transport 105 | 106 | def connection_lost(self, exc): 107 | if self._negotiate_done and self._app_protocol is not self: 108 | self._loop.call_soon(self._app_protocol.connection_lost, exc) 109 | super().connection_lost(exc) 110 | 111 | def pause_writing(self): 112 | if self._negotiate_done and self._app_protocol is not self: 113 | self._app_protocol.pause_writing() 114 | else: 115 | super().pause_writing() 116 | 117 | def resume_writing(self): 118 | if self._negotiate_done and self._app_protocol is not self: 119 | self._app_protocol.resume_writing() 120 | else: 121 | super().resume_writing() 122 | 123 | def data_received(self, data): 124 | if self._negotiate_done and self._app_protocol is not self: 125 | self._app_protocol.data_received(data) 126 | else: 127 | super().data_received(data) 128 | 129 | def eof_received(self): 130 | if self._negotiate_done and self._app_protocol is not self: 131 | self._app_protocol.eof_received() 132 | super().eof_received() 133 | 134 | async def socks_request(self, cmd): 135 | raise NotImplementedError 136 | 137 | def write_request(self, request): 138 | bdata = bytearray() 139 | 140 | for item in request: 141 | if isinstance(item, int): 142 | bdata.append(item) 143 | elif isinstance(item, (bytearray, bytes)): 144 | bdata += item 145 | else: 146 | raise ValueError('Unsupported item') 147 | self._stream_writer.write(bdata) 148 | 149 | async def read_response(self, n): 150 | try: 151 | return (await self._stream_reader.readexactly(n)) 152 | except asyncio.IncompleteReadError as e: 153 | raise InvalidServerReply( 154 | 'Server sent fewer bytes than required (%s)' % str(e)) 155 | 156 | async def _get_dst_addr(self): 157 | infos = await self._loop.getaddrinfo( 158 | self._dst_host, self._dst_port, family=socket.AF_UNSPEC, 159 | type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP, 160 | flags=socket.AI_ADDRCONFIG) 161 | if not infos: 162 | raise OSError('getaddrinfo() returned empty list') 163 | return infos[0][0], infos[0][4][0] 164 | 165 | @property 166 | def app_protocol(self): 167 | return self._app_protocol 168 | 169 | @property 170 | def app_transport(self): 171 | return self._transport 172 | 173 | @property 174 | def proxy_sockname(self): 175 | """ 176 | Returns the bound IP address and port number at the proxy. 177 | """ 178 | return self._proxy_sockname 179 | 180 | @property 181 | def proxy_peername(self): 182 | """ 183 | Returns the IP and port number of the proxy. 184 | """ 185 | sock = self._transport.get_extra_info('socket') 186 | return sock.peername if sock else None 187 | 188 | @property 189 | def peername(self): 190 | """ 191 | Returns the IP address and port number of the destination 192 | machine (note: get_proxy_peername returns the proxy) 193 | """ 194 | return self._proxy_peername 195 | 196 | @property 197 | def reader(self): 198 | return self._stream_reader 199 | 200 | @property 201 | def writer(self): 202 | return self._stream_writer 203 | 204 | 205 | class Socks4Protocol(BaseSocksProtocol): 206 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, 207 | remote_resolve=True, loop=None, ssl=False, 208 | server_hostname=None, negotiate_done_cb=None, 209 | reader_limit=DEFAULT_LIMIT): 210 | proxy_auth = proxy_auth or Socks4Auth('') 211 | 212 | if not isinstance(proxy, Socks4Addr): 213 | raise ValueError('Invalid proxy format') 214 | 215 | if not isinstance(proxy_auth, Socks4Auth): 216 | raise ValueError('Invalid proxy_auth format') 217 | 218 | super().__init__(proxy, proxy_auth, dst, app_protocol_factory, 219 | waiter, remote_resolve=remote_resolve, loop=loop, 220 | ssl=ssl, server_hostname=server_hostname, 221 | reader_limit=reader_limit, 222 | negotiate_done_cb=negotiate_done_cb) 223 | 224 | async def socks_request(self, cmd): 225 | # prepare destination addr/port 226 | host, port = self._dst_host, self._dst_port 227 | port_bytes = struct.pack(b'>H', port) 228 | include_hostname = False 229 | 230 | try: 231 | host_bytes = socket.inet_aton(host) 232 | except socket.error: 233 | if self._remote_resolve: 234 | host_bytes = bytes([c.NULL, c.NULL, c.NULL, 0x01]) 235 | include_hostname = True 236 | else: 237 | # it's not an IP number, so it's probably a DNS name. 238 | family, host = await self._get_dst_addr() 239 | host_bytes = socket.inet_aton(host) 240 | 241 | # build and send connect command 242 | req = [c.SOCKS_VER4, cmd, port_bytes, 243 | host_bytes, self._auth.login, c.NULL] 244 | if include_hostname: 245 | req += [self._dst_host.encode('idna'), c.NULL] 246 | 247 | self.write_request(req) 248 | 249 | # read/process result 250 | resp = await self.read_response(8) 251 | 252 | if resp[0] != c.NULL: 253 | raise InvalidServerReply('SOCKS4 proxy server sent invalid data') 254 | if resp[1] != c.SOCKS4_GRANTED: 255 | error = c.SOCKS4_ERRORS.get(resp[1], 'Unknown error') 256 | raise SocksError('[Errno {0:#04x}]: {1}'.format(resp[1], error)) 257 | 258 | binded = socket.inet_ntoa(resp[4:]), struct.unpack('>H', resp[2:4])[0] 259 | return (host, port), binded 260 | 261 | 262 | class Socks5Protocol(BaseSocksProtocol): 263 | def __init__(self, proxy, proxy_auth, dst, app_protocol_factory, waiter, 264 | remote_resolve=True, loop=None, ssl=False, 265 | server_hostname=None, negotiate_done_cb=None, 266 | reader_limit=DEFAULT_LIMIT): 267 | proxy_auth = proxy_auth or Socks5Auth('', '') 268 | 269 | if not isinstance(proxy, Socks5Addr): 270 | raise ValueError('Invalid proxy format') 271 | 272 | if not isinstance(proxy_auth, Socks5Auth): 273 | raise ValueError('Invalid proxy_auth format') 274 | 275 | super().__init__(proxy, proxy_auth, dst, app_protocol_factory, 276 | waiter, remote_resolve=remote_resolve, loop=loop, 277 | ssl=ssl, server_hostname=server_hostname, 278 | reader_limit=reader_limit, 279 | negotiate_done_cb=negotiate_done_cb) 280 | 281 | async def socks_request(self, cmd): 282 | await self.authenticate() 283 | 284 | # build and send command 285 | dst_addr, resolved = await self.build_dst_address( 286 | self._dst_host, self._dst_port) 287 | self.write_request([c.SOCKS_VER5, cmd, c.RSV] + dst_addr) 288 | 289 | # read/process command response 290 | resp = await self.read_response(3) 291 | 292 | if resp[0] != c.SOCKS_VER5: 293 | raise InvalidServerVersion( 294 | 'SOCKS5 proxy server sent invalid version' 295 | ) 296 | if resp[1] != c.SOCKS5_GRANTED: 297 | error = c.SOCKS5_ERRORS.get(resp[1], 'Unknown error') 298 | raise SocksError('[Errno {0:#04x}]: {1}'.format(resp[1], error)) 299 | 300 | binded = await self.read_address() 301 | 302 | return resolved, binded 303 | 304 | async def authenticate(self): 305 | # send available auth methods 306 | if self._auth.login and self._auth.password: 307 | req = [c.SOCKS_VER5, 0x02, 308 | c.SOCKS5_AUTH_ANONYMOUS, c.SOCKS5_AUTH_UNAME_PWD] 309 | else: 310 | req = [c.SOCKS_VER5, 0x01, c.SOCKS5_AUTH_ANONYMOUS] 311 | 312 | self.write_request(req) 313 | 314 | # read/process response and send auth data if necessary 315 | chosen_auth = await self.read_response(2) 316 | 317 | if chosen_auth[0] != c.SOCKS_VER5: 318 | raise InvalidServerVersion( 319 | 'SOCKS5 proxy server sent invalid version' 320 | ) 321 | 322 | if chosen_auth[1] == c.SOCKS5_AUTH_UNAME_PWD: 323 | req = [0x01, chr(len(self._auth.login)).encode(), self._auth.login, 324 | chr(len(self._auth.password)).encode(), self._auth.password] 325 | self.write_request(req) 326 | 327 | auth_status = await self.read_response(2) 328 | if auth_status[0] != 0x01: 329 | raise InvalidServerReply( 330 | 'SOCKS5 proxy server sent invalid data' 331 | ) 332 | if auth_status[1] != c.SOCKS5_GRANTED: 333 | raise LoginAuthenticationFailed( 334 | "SOCKS5 authentication failed" 335 | ) 336 | # offered auth methods rejected 337 | elif chosen_auth[1] != c.SOCKS5_AUTH_ANONYMOUS: 338 | if chosen_auth[1] == c.SOCKS5_AUTH_NO_ACCEPTABLE_METHODS: 339 | raise NoAcceptableAuthMethods( 340 | 'All offered SOCKS5 authentication methods were rejected' 341 | ) 342 | else: 343 | raise InvalidServerReply( 344 | 'SOCKS5 proxy server sent invalid data' 345 | ) 346 | 347 | async def build_dst_address(self, host, port): 348 | family_to_byte = {socket.AF_INET: c.SOCKS5_ATYP_IPv4, 349 | socket.AF_INET6: c.SOCKS5_ATYP_IPv6} 350 | port_bytes = struct.pack('>H', port) 351 | 352 | # if the given destination address is an IP address, we will 353 | # use the IP address request even if remote resolving was specified. 354 | for family in (socket.AF_INET, socket.AF_INET6): 355 | try: 356 | host_bytes = socket.inet_pton(family, host) 357 | req = [family_to_byte[family], host_bytes, port_bytes] 358 | return req, (host, port) 359 | except socket.error: 360 | pass 361 | 362 | # it's not an IP number, so it's probably a DNS name. 363 | if self._remote_resolve: 364 | host_bytes = host.encode('idna') 365 | req = [c.SOCKS5_ATYP_DOMAIN, chr(len(host_bytes)).encode(), 366 | host_bytes, port_bytes] 367 | else: 368 | family, host_bytes = await self._get_dst_addr() 369 | host_bytes = socket.inet_pton(family, host_bytes) 370 | req = [family_to_byte[family], host_bytes, port_bytes] 371 | host = socket.inet_ntop(family, host_bytes) 372 | 373 | return req, (host, port) 374 | 375 | async def read_address(self): 376 | atype = await self.read_response(1) 377 | 378 | if atype[0] == c.SOCKS5_ATYP_IPv4: 379 | addr = socket.inet_ntoa((await self.read_response(4))) 380 | elif atype[0] == c.SOCKS5_ATYP_DOMAIN: 381 | length = await self.read_response(1) 382 | addr = await self.read_response(ord(length)) 383 | elif atype[0] == c.SOCKS5_ATYP_IPv6: 384 | addr = await self.read_response(16) 385 | addr = socket.inet_ntop(socket.AF_INET6, addr) 386 | else: 387 | raise InvalidServerReply('SOCKS5 proxy server sent invalid data') 388 | 389 | port = await self.read_response(2) 390 | port = struct.unpack('>H', port)[0] 391 | 392 | return addr, port 393 | -------------------------------------------------------------------------------- /aiosocks/test_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import struct 3 | import socket 4 | from aiohttp.test_utils import unused_port 5 | 6 | 7 | class FakeSocksSrv: 8 | def __init__(self, loop, write_buff): 9 | self._loop = loop 10 | self._write_buff = write_buff 11 | self._transports = [] 12 | self._srv = None 13 | self.port = unused_port() 14 | 15 | async def __aenter__(self): 16 | transports = self._transports 17 | write_buff = self._write_buff 18 | 19 | class SocksPrimitiveProtocol(asyncio.Protocol): 20 | _transport = None 21 | 22 | def connection_made(self, transport): 23 | self._transport = transport 24 | transports.append(transport) 25 | 26 | def data_received(self, data): 27 | self._transport.write(write_buff) 28 | 29 | def factory(): 30 | return SocksPrimitiveProtocol() 31 | 32 | self._srv = await self._loop.create_server( 33 | factory, '127.0.0.1', self.port) 34 | 35 | return self 36 | 37 | async def __aexit__(self, exc_type, exc_val, exc_tb): 38 | for tr in self._transports: 39 | tr.close() 40 | 41 | self._srv.close() 42 | await self._srv.wait_closed() 43 | 44 | 45 | class FakeSocks4Srv: 46 | def __init__(self, loop): 47 | self._loop = loop 48 | self._transports = [] 49 | self._futures = [] 50 | self._srv = None 51 | self.port = unused_port() 52 | 53 | async def __aenter__(self): 54 | transports = self._transports 55 | futures = self._futures 56 | 57 | class Socks4Protocol(asyncio.StreamReaderProtocol): 58 | def __init__(self, _loop): 59 | self._loop = _loop 60 | reader = asyncio.StreamReader(loop=self._loop) 61 | super().__init__(reader, client_connected_cb=self.negotiate, 62 | loop=self._loop) 63 | 64 | def connection_made(self, transport): 65 | transports.append(transport) 66 | super().connection_made(transport) 67 | 68 | async def negotiate(self, reader, writer): 69 | writer.write(b'\x00\x5a\x04W\x01\x01\x01\x01') 70 | 71 | data = await reader.read(9) 72 | 73 | dst_port = struct.unpack('>H', data[2:4])[0] 74 | dst_addr = data[4:8] 75 | 76 | if data[-1] != 0x00: 77 | while True: 78 | byte = await reader.read(1) 79 | if byte == 0x00: 80 | break 81 | 82 | if dst_addr == b'\x00\x00\x00\x01': 83 | dst_addr = bytearray() 84 | 85 | while True: 86 | byte = await reader.read(1) 87 | if byte == 0x00: 88 | break 89 | dst_addr.append(byte) 90 | else: 91 | dst_addr = socket.inet_ntoa(dst_addr) 92 | 93 | cl_reader, cl_writer = await asyncio.open_connection( 94 | host=dst_addr, port=dst_port, loop=self._loop 95 | ) 96 | transports.append(cl_writer) 97 | 98 | cl_fut = asyncio.ensure_future( 99 | self.retranslator(reader, cl_writer), loop=self._loop) 100 | dst_fut = asyncio.ensure_future( 101 | self.retranslator(cl_reader, writer), loop=self._loop) 102 | 103 | futures.append(cl_fut) 104 | futures.append(dst_fut) 105 | 106 | async def retranslator(self, reader, writer): 107 | data = bytearray() 108 | while True: 109 | try: 110 | byte = await reader.read(10) 111 | if not byte: 112 | break 113 | data.append(byte[0]) 114 | writer.write(byte) 115 | await writer.drain() 116 | except: # noqa 117 | break 118 | 119 | def factory(): 120 | return Socks4Protocol(_loop=self._loop) 121 | 122 | self._srv = await self._loop.create_server( 123 | factory, '127.0.0.1', self.port) 124 | 125 | return self 126 | 127 | async def __aexit__(self, exc_type, exc_val, exc_tb): 128 | for tr in self._transports: 129 | tr.close() 130 | 131 | self._srv.close() 132 | await self._srv.wait_closed() 133 | 134 | for f in self._futures: 135 | if not f.cancelled() or not f.done(): 136 | f.cancel() 137 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import codecs 3 | import os 4 | import re 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | 13 | with codecs.open(os.path.join(os.path.abspath(os.path.dirname( 14 | __file__)), 'aiosocks', '__init__.py'), 'r', 'latin1') as fp: 15 | try: 16 | version = re.findall(r"^__version__ = '([^']+)'\r?$", 17 | fp.read(), re.M)[0] 18 | except IndexError: 19 | raise RuntimeError('Unable to determine version.') 20 | 21 | 22 | if sys.version_info < (3, 5, 3): 23 | raise RuntimeError("aiosocks requires Python 3.5.3+") 24 | 25 | 26 | setup( 27 | name='aiosocks', 28 | author='Nail Ibragimov', 29 | author_email='ibragwork@gmail.com', 30 | version=version, 31 | license='Apache 2', 32 | url='https://github.com/nibrag/aiosocks', 33 | 34 | description='SOCKS proxy client for asyncio and aiohttp', 35 | long_description=open("README.rst").read(), 36 | classifiers=( 37 | "License :: OSI Approved :: Apache Software License" 38 | "Programming Language :: Python :: 3 :: Only", 39 | "Programming Language :: Python :: 3.5", 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | ), 44 | packages=['aiosocks'], 45 | install_requires=[ 46 | 'aiohttp>=3.4', 47 | ] 48 | ) 49 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from aiohttp.pytest_plugin import * # noqa 2 | -------------------------------------------------------------------------------- /tests/sample.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICMzCCAZwCCQDFl4ys0fU7iTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQGEwJV 3 | UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuLUZyYW5jaXNjbzEi 4 | MCAGA1UECgwZUHl0aG9uIFNvZnR3YXJlIEZvbmRhdGlvbjAeFw0xMzAzMTgyMDA3 5 | MjhaFw0yMzAzMTYyMDA3MjhaMF4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp 6 | Zm9ybmlhMRYwFAYDVQQHDA1TYW4tRnJhbmNpc2NvMSIwIAYDVQQKDBlQeXRob24g 7 | U29mdHdhcmUgRm9uZGF0aW9uMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCn 8 | t3s+J7L0xP/YdAQOacpPi9phlrzKZhcXL3XMu2LCUg2fNJpx/47Vc5TZSaO11uO7 9 | gdwVz3Z7Q2epAgwo59JLffLt5fia8+a/SlPweI/j4+wcIIIiqusnLfpqR8cIAavg 10 | Z06cLYCDvb9wMlheIvSJY12skc1nnphWS2YJ0Xm6uQIDAQABMA0GCSqGSIb3DQEB 11 | BQUAA4GBAE9PknG6pv72+5z/gsDGYy8sK5UNkbWSNr4i4e5lxVsF03+/M71H+3AB 12 | MxVX4+A+Vlk2fmU+BrdHIIUE0r1dDcO3josQ9hc9OJpp5VLSQFP8VeuJCmzYPp9I 13 | I8WbW93cnXnChTrYQVdgVoFdv7GE9YgU7NYkrGIM0nZl1/f/bHPB 14 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tests/sample.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQCnt3s+J7L0xP/YdAQOacpPi9phlrzKZhcXL3XMu2LCUg2fNJpx 3 | /47Vc5TZSaO11uO7gdwVz3Z7Q2epAgwo59JLffLt5fia8+a/SlPweI/j4+wcIIIi 4 | qusnLfpqR8cIAavgZ06cLYCDvb9wMlheIvSJY12skc1nnphWS2YJ0Xm6uQIDAQAB 5 | AoGABfm8k19Yue3W68BecKEGS0VBV57GRTPT+MiBGvVGNIQ15gk6w3sGfMZsdD1y 6 | bsUkQgcDb2d/4i5poBTpl/+Cd41V+c20IC/sSl5X1IEreHMKSLhy/uyjyiyfXlP1 7 | iXhToFCgLWwENWc8LzfUV8vuAV5WG6oL9bnudWzZxeqx8V0CQQDR7xwVj6LN70Eb 8 | DUhSKLkusmFw5Gk9NJ/7wZ4eHg4B8c9KNVvSlLCLhcsVTQXuqYeFpOqytI45SneP 9 | lr0vrvsDAkEAzITYiXu6ox5huDCG7imX2W9CAYuX638urLxBqBXMS7GqBzojD6RL 10 | 21Q8oPwJWJquERa3HDScq1deiQbM9uKIkwJBAIa1PLslGN216Xv3UPHPScyKD/aF 11 | ynXIv+OnANPoiyp6RH4ksQ/18zcEGiVH8EeNpvV9tlAHhb+DZibQHgNr74sCQQC0 12 | zhToplu/bVKSlUQUNO0rqrI9z30FErDewKeCw5KSsIRSU1E/uM3fHr9iyq4wiL6u 13 | GNjUtKZ0y46lsT9uW6LFAkB5eqeEQnshAdr3X5GykWHJ8DDGBXPPn6Rce1NX4RSq 14 | V9khG2z1bFyfo+hMqpYnF2k32hVq3E54RS8YYnwBsVof 15 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /tests/test_connector.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import ssl 3 | 4 | import aiosocks 5 | import aiohttp 6 | import pytest 7 | from yarl import URL 8 | from unittest import mock 9 | from aiohttp.test_utils import make_mocked_coro 10 | from aiohttp import BasicAuth, ClientTimeout 11 | from aiosocks.connector import ProxyConnector, ProxyClientRequest 12 | from aiosocks.helpers import Socks4Auth, Socks5Auth 13 | 14 | 15 | async def test_connect_proxy_ip(loop): 16 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 17 | 18 | with mock.patch('aiosocks.connector.create_connection', 19 | make_mocked_coro((tr, proto))): 20 | loop.getaddrinfo = make_mocked_coro( 21 | [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) 22 | 23 | req = ProxyClientRequest( 24 | 'GET', URL('http://python.org'), loop=loop, 25 | proxy=URL('socks5://proxy.org')) 26 | connector = ProxyConnector(loop=loop) 27 | conn = await connector.connect(req, [], ClientTimeout()) 28 | 29 | assert loop.getaddrinfo.called 30 | assert conn.protocol is proto 31 | 32 | conn.close() 33 | 34 | 35 | async def test_connect_proxy_domain(): 36 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 37 | 38 | with mock.patch('aiosocks.connector.create_connection', 39 | make_mocked_coro((tr, proto))): 40 | loop_mock = mock.Mock() 41 | 42 | req = ProxyClientRequest( 43 | 'GET', URL('http://python.org'), loop=loop_mock, 44 | proxy=URL('socks5://proxy.example')) 45 | connector = ProxyConnector(loop=loop_mock) 46 | 47 | connector._resolve_host = make_mocked_coro([mock.MagicMock()]) 48 | conn = await connector.connect(req, [], ClientTimeout()) 49 | 50 | assert connector._resolve_host.call_count == 1 51 | assert conn.protocol is proto 52 | 53 | conn.close() 54 | 55 | 56 | async def test_connect_remote_resolve(loop): 57 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 58 | 59 | with mock.patch('aiosocks.connector.create_connection', 60 | make_mocked_coro((tr, proto))): 61 | req = ProxyClientRequest( 62 | 'GET', URL('http://python.org'), loop=loop, 63 | proxy=URL('socks5://127.0.0.1')) 64 | connector = ProxyConnector(loop=loop, remote_resolve=True) 65 | connector._resolve_host = make_mocked_coro([mock.MagicMock()]) 66 | conn = await connector.connect(req, [], ClientTimeout()) 67 | 68 | assert connector._resolve_host.call_count == 1 69 | assert conn.protocol is proto 70 | 71 | conn.close() 72 | 73 | 74 | async def test_connect_locale_resolve(loop): 75 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 76 | 77 | with mock.patch('aiosocks.connector.create_connection', 78 | make_mocked_coro((tr, proto))): 79 | req = ProxyClientRequest( 80 | 'GET', URL('http://python.org'), loop=loop, 81 | proxy=URL('socks5://proxy.example')) 82 | connector = ProxyConnector(loop=loop, remote_resolve=False) 83 | connector._resolve_host = make_mocked_coro([mock.MagicMock()]) 84 | conn = await connector.connect(req, [], ClientTimeout()) 85 | 86 | assert connector._resolve_host.call_count == 2 87 | assert conn.protocol is proto 88 | 89 | conn.close() 90 | 91 | 92 | @pytest.mark.parametrize('remote_resolve', [True, False]) 93 | async def test_resolve_host_fail(loop, remote_resolve): 94 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 95 | 96 | with mock.patch('aiosocks.connector.create_connection', 97 | make_mocked_coro((tr, proto))): 98 | req = ProxyClientRequest( 99 | 'GET', URL('http://python.org'), loop=loop, 100 | proxy=URL('socks5://proxy.example')) 101 | connector = ProxyConnector(loop=loop, remote_resolve=remote_resolve) 102 | connector._resolve_host = make_mocked_coro(raise_exception=OSError()) 103 | 104 | with pytest.raises(aiohttp.ClientConnectorError): 105 | await connector.connect(req, [], ClientTimeout()) 106 | 107 | 108 | @pytest.mark.parametrize('exc', [ 109 | (ssl.CertificateError, aiohttp.ClientConnectorCertificateError), 110 | (ssl.SSLError, aiohttp.ClientConnectorSSLError), 111 | (aiosocks.SocksConnectionError, aiohttp.ClientProxyConnectionError)]) 112 | async def test_proxy_connect_fail(loop, exc): 113 | loop_mock = mock.Mock() 114 | loop_mock.getaddrinfo = make_mocked_coro( 115 | [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) 116 | cc_coro = make_mocked_coro( 117 | raise_exception=exc[0]()) 118 | 119 | with mock.patch('aiosocks.connector.create_connection', cc_coro): 120 | req = ProxyClientRequest( 121 | 'GET', URL('http://python.org'), loop=loop, 122 | proxy=URL('socks5://127.0.0.1')) 123 | connector = ProxyConnector(loop=loop_mock) 124 | 125 | with pytest.raises(exc[1]): 126 | await connector.connect(req, [], ClientTimeout()) 127 | 128 | 129 | async def test_proxy_negotiate_fail(loop): 130 | loop_mock = mock.Mock() 131 | loop_mock.getaddrinfo = make_mocked_coro( 132 | [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) 133 | 134 | with mock.patch('aiosocks.connector.create_connection', 135 | make_mocked_coro(raise_exception=aiosocks.SocksError())): 136 | req = ProxyClientRequest( 137 | 'GET', URL('http://python.org'), loop=loop, 138 | proxy=URL('socks5://127.0.0.1')) 139 | connector = ProxyConnector(loop=loop_mock) 140 | 141 | with pytest.raises(aiosocks.SocksError): 142 | await connector.connect(req, [], ClientTimeout()) 143 | 144 | 145 | async def test_proxy_connect_http(loop): 146 | tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') 147 | loop_mock = mock.Mock() 148 | loop_mock.getaddrinfo = make_mocked_coro([ 149 | [0, 0, 0, 0, ['127.0.0.1', 1080]]]) 150 | loop_mock.create_connection = make_mocked_coro((tr, proto)) 151 | loop_mock.create_task.return_value = asyncio.Task( 152 | make_mocked_coro([ 153 | {'host': 'host', 'port': 80, 'family': 1, 154 | 'hostname': 'hostname', 'flags': 11, 'proto': 'proto'}])()) 155 | 156 | req = ProxyClientRequest( 157 | 'GET', URL('http://python.org'), loop=loop, 158 | proxy=URL('http://127.0.0.1')) 159 | connector = ProxyConnector(loop=loop_mock) 160 | 161 | await connector.connect(req, [], ClientTimeout()) 162 | 163 | 164 | @pytest.mark.parametrize('proxy', [ 165 | (URL('socks4://proxy.org'), Socks4Auth('login')), 166 | (URL('socks5://proxy.org'), Socks5Auth('login', 'password')), 167 | (URL('http://proxy.org'), BasicAuth('login')), (None, BasicAuth('login')), 168 | (URL('socks4://proxy.org'), None), (None, None)]) 169 | def test_proxy_client_request_valid(proxy, loop): 170 | proxy, proxy_auth = proxy 171 | p = ProxyClientRequest('GET', URL('http://python.org'), 172 | proxy=proxy, proxy_auth=proxy_auth, loop=loop) 173 | assert p.proxy is proxy 174 | assert p.proxy_auth is proxy_auth 175 | 176 | 177 | def test_proxy_client_request_invalid(loop): 178 | with pytest.raises(ValueError) as cm: 179 | ProxyClientRequest( 180 | 'GET', URL('http://python.org'), 181 | proxy=URL('socks6://proxy.org'), proxy_auth=None, loop=loop) 182 | assert 'Only http, socks4 and socks5 proxies are supported' \ 183 | in str(cm.value) 184 | 185 | with pytest.raises(ValueError) as cm: 186 | ProxyClientRequest( 187 | 'GET', URL('http://python.org'), loop=loop, 188 | proxy=URL('http://proxy.org'), proxy_auth=Socks4Auth('l')) 189 | assert 'proxy_auth must be None or BasicAuth() ' \ 190 | 'tuple for http proxy' in str(cm.value) 191 | 192 | with pytest.raises(ValueError) as cm: 193 | ProxyClientRequest( 194 | 'GET', URL('http://python.org'), loop=loop, 195 | proxy=URL('socks4://proxy.org'), proxy_auth=BasicAuth('l')) 196 | assert 'proxy_auth must be None or Socks4Auth() ' \ 197 | 'tuple for socks4 proxy' in str(cm.value) 198 | 199 | with pytest.raises(ValueError) as cm: 200 | ProxyClientRequest( 201 | 'GET', URL('http://python.org'), loop=loop, 202 | proxy=URL('socks5://proxy.org'), proxy_auth=Socks4Auth('l')) 203 | assert 'proxy_auth must be None or Socks5Auth() ' \ 204 | 'tuple for socks5 proxy' in str(cm.value) 205 | -------------------------------------------------------------------------------- /tests/test_create_connect.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import aiosocks 3 | from aiohttp.test_utils import make_mocked_coro 4 | from unittest import mock 5 | 6 | 7 | async def test_create_connection_init(): 8 | addr = aiosocks.Socks5Addr('localhost') 9 | auth = aiosocks.Socks5Auth('usr', 'pwd') 10 | dst = ('python.org', 80) 11 | 12 | # proxy argument 13 | with pytest.raises(AssertionError) as ct: 14 | await aiosocks.create_connection(None, None, auth, dst) 15 | assert 'proxy must be Socks4Addr() or Socks5Addr() tuple' in str(ct.value) 16 | 17 | with pytest.raises(AssertionError) as ct: 18 | await aiosocks.create_connection(None, auth, auth, dst) 19 | assert 'proxy must be Socks4Addr() or Socks5Addr() tuple' in str(ct.value) 20 | 21 | # proxy_auth 22 | with pytest.raises(AssertionError) as ct: 23 | await aiosocks.create_connection(None, addr, addr, dst) 24 | assert 'proxy_auth must be None or Socks4Auth()' in str(ct.value) 25 | 26 | # dst 27 | with pytest.raises(AssertionError) as ct: 28 | await aiosocks.create_connection(None, addr, auth, None) 29 | assert 'invalid dst format, tuple("dst_host", dst_port))' in str(ct.value) 30 | 31 | # addr and auth compatibility 32 | with pytest.raises(ValueError) as ct: 33 | await aiosocks.create_connection( 34 | None, addr, aiosocks.Socks4Auth(''), dst) 35 | assert 'proxy is Socks5Addr but proxy_auth is not Socks5Auth' \ 36 | in str(ct.value) 37 | 38 | with pytest.raises(ValueError) as ct: 39 | await aiosocks.create_connection( 40 | None, aiosocks.Socks4Addr(''), auth, dst) 41 | assert 'proxy is Socks4Addr but proxy_auth is not Socks4Auth' \ 42 | in str(ct.value) 43 | 44 | # test ssl, server_hostname 45 | with pytest.raises(ValueError) as ct: 46 | await aiosocks.create_connection( 47 | None, addr, auth, dst, server_hostname='python.org') 48 | assert 'server_hostname is only meaningful with ssl' in str(ct.value) 49 | 50 | 51 | async def test_connection_fail(): 52 | addr = aiosocks.Socks5Addr('localhost') 53 | auth = aiosocks.Socks5Auth('usr', 'pwd') 54 | dst = ('python.org', 80) 55 | 56 | loop_mock = mock.Mock() 57 | loop_mock.create_connection = make_mocked_coro(raise_exception=OSError()) 58 | 59 | with pytest.raises(aiosocks.SocksConnectionError): 60 | await aiosocks.create_connection( 61 | None, addr, auth, dst, loop=loop_mock) 62 | 63 | 64 | async def test_negotiate_fail(): 65 | addr = aiosocks.Socks5Addr('localhost') 66 | auth = aiosocks.Socks5Auth('usr', 'pwd') 67 | dst = ('python.org', 80) 68 | 69 | loop_mock = mock.Mock() 70 | loop_mock.create_connection = make_mocked_coro((mock.Mock(), mock.Mock())) 71 | 72 | with mock.patch('aiosocks.asyncio.Future') as future_mock: 73 | future_mock.side_effect = make_mocked_coro( 74 | raise_exception=aiosocks.SocksError()) 75 | 76 | with pytest.raises(aiosocks.SocksError): 77 | await aiosocks.create_connection( 78 | None, addr, auth, dst, loop=loop_mock) 79 | 80 | 81 | async def test_open_connection(): 82 | addr = aiosocks.Socks5Addr('localhost') 83 | auth = aiosocks.Socks5Auth('usr', 'pwd') 84 | dst = ('python.org', 80) 85 | 86 | transp, proto = mock.Mock(), mock.Mock() 87 | reader, writer = mock.Mock(), mock.Mock() 88 | 89 | proto.app_protocol.reader, proto.app_protocol.writer = reader, writer 90 | 91 | loop_mock = mock.Mock() 92 | loop_mock.create_connection = make_mocked_coro((transp, proto)) 93 | 94 | with mock.patch('aiosocks.asyncio.Future') as future_mock: 95 | future_mock.side_effect = make_mocked_coro(True) 96 | r, w = await aiosocks.open_connection(addr, auth, dst, loop=loop_mock) 97 | 98 | assert reader is r 99 | assert writer is w 100 | -------------------------------------------------------------------------------- /tests/test_functional.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import aiosocks 3 | import aiohttp 4 | import os 5 | import ssl 6 | from aiohttp import web 7 | from aiohttp.test_utils import RawTestServer 8 | from aiosocks.test_utils import FakeSocksSrv, FakeSocks4Srv 9 | from aiosocks.connector import ProxyConnector, ProxyClientRequest 10 | 11 | 12 | async def test_socks4_connect_success(loop): 13 | pld = b'\x00\x5a\x04W\x01\x01\x01\x01test' 14 | 15 | async with FakeSocksSrv(loop, pld) as srv: 16 | addr = aiosocks.Socks4Addr('127.0.0.1', srv.port) 17 | auth = aiosocks.Socks4Auth('usr') 18 | dst = ('python.org', 80) 19 | 20 | transport, protocol = await aiosocks.create_connection( 21 | None, addr, auth, dst, loop=loop) 22 | 23 | assert protocol.proxy_sockname == ('1.1.1.1', 1111) 24 | 25 | data = await protocol._stream_reader.read(4) 26 | assert data == b'test' 27 | 28 | transport.close() 29 | 30 | 31 | async def test_socks4_invalid_data(loop): 32 | pld = b'\x01\x5a\x04W\x01\x01\x01\x01' 33 | 34 | async with FakeSocksSrv(loop, pld) as srv: 35 | addr = aiosocks.Socks4Addr('127.0.0.1', srv.port) 36 | auth = aiosocks.Socks4Auth('usr') 37 | dst = ('python.org', 80) 38 | 39 | with pytest.raises(aiosocks.SocksError) as ct: 40 | await aiosocks.create_connection( 41 | None, addr, auth, dst, loop=loop) 42 | assert 'invalid data' in str(ct.value) 43 | 44 | 45 | async def test_socks4_srv_error(loop): 46 | pld = b'\x00\x5b\x04W\x01\x01\x01\x01' 47 | 48 | async with FakeSocksSrv(loop, pld) as srv: 49 | addr = aiosocks.Socks4Addr('127.0.0.1', srv.port) 50 | auth = aiosocks.Socks4Auth('usr') 51 | dst = ('python.org', 80) 52 | 53 | with pytest.raises(aiosocks.SocksError) as ct: 54 | await aiosocks.create_connection( 55 | None, addr, auth, dst, loop=loop) 56 | assert '0x5b' in str(ct.value) 57 | 58 | 59 | async def test_socks5_connect_success_anonymous(loop): 60 | pld = b'\x05\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04Wtest' 61 | 62 | async with FakeSocksSrv(loop, pld) as srv: 63 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 64 | auth = aiosocks.Socks5Auth('usr', 'pwd') 65 | dst = ('python.org', 80) 66 | 67 | transport, protocol = await aiosocks.create_connection( 68 | None, addr, auth, dst, loop=loop) 69 | 70 | assert protocol.proxy_sockname == ('1.1.1.1', 1111) 71 | 72 | data = await protocol._stream_reader.read(4) 73 | assert data == b'test' 74 | 75 | transport.close() 76 | 77 | 78 | async def test_socks5_connect_success_usr_pwd(loop): 79 | pld = b'\x05\x02\x01\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04Wtest' 80 | 81 | async with FakeSocksSrv(loop, pld) as srv: 82 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 83 | auth = aiosocks.Socks5Auth('usr', 'pwd') 84 | dst = ('python.org', 80) 85 | 86 | transport, protocol = await aiosocks.create_connection( 87 | None, addr, auth, dst, loop=loop) 88 | assert protocol.proxy_sockname == ('1.1.1.1', 1111) 89 | 90 | data = await protocol._stream_reader.read(4) 91 | assert data == b'test' 92 | transport.close() 93 | 94 | 95 | async def test_socks5_auth_ver_err(loop): 96 | async with FakeSocksSrv(loop, b'\x04\x02') as srv: 97 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 98 | auth = aiosocks.Socks5Auth('usr', 'pwd') 99 | dst = ('python.org', 80) 100 | 101 | with pytest.raises(aiosocks.SocksError) as ct: 102 | await aiosocks.create_connection( 103 | None, addr, auth, dst, loop=loop) 104 | assert 'invalid version' in str(ct.value) 105 | 106 | 107 | async def test_socks5_auth_method_rejected(loop): 108 | async with FakeSocksSrv(loop, b'\x05\xFF') as srv: 109 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 110 | auth = aiosocks.Socks5Auth('usr', 'pwd') 111 | dst = ('python.org', 80) 112 | 113 | with pytest.raises(aiosocks.SocksError) as ct: 114 | await aiosocks.create_connection( 115 | None, addr, auth, dst, loop=loop) 116 | assert 'authentication methods were rejected' in str(ct.value) 117 | 118 | 119 | async def test_socks5_auth_status_invalid(loop): 120 | async with FakeSocksSrv(loop, b'\x05\xF0') as srv: 121 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 122 | auth = aiosocks.Socks5Auth('usr', 'pwd') 123 | dst = ('python.org', 80) 124 | 125 | with pytest.raises(aiosocks.SocksError) as ct: 126 | await aiosocks.create_connection( 127 | None, addr, auth, dst, loop=loop) 128 | assert 'invalid data' in str(ct.value) 129 | 130 | 131 | async def test_socks5_auth_status_invalid2(loop): 132 | async with FakeSocksSrv(loop, b'\x05\x02\x02\x00') as srv: 133 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 134 | auth = aiosocks.Socks5Auth('usr', 'pwd') 135 | dst = ('python.org', 80) 136 | 137 | with pytest.raises(aiosocks.SocksError) as ct: 138 | await aiosocks.create_connection( 139 | None, addr, auth, dst, loop=loop) 140 | assert 'invalid data' in str(ct.value) 141 | 142 | 143 | async def test_socks5_auth_failed(loop): 144 | async with FakeSocksSrv(loop, b'\x05\x02\x01\x01') as srv: 145 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 146 | auth = aiosocks.Socks5Auth('usr', 'pwd') 147 | dst = ('python.org', 80) 148 | 149 | with pytest.raises(aiosocks.SocksError) as ct: 150 | await aiosocks.create_connection( 151 | None, addr, auth, dst, loop=loop) 152 | assert 'authentication failed' in str(ct.value) 153 | 154 | 155 | async def test_socks5_cmd_ver_err(loop): 156 | async with FakeSocksSrv(loop, b'\x05\x02\x01\x00\x04\x00\x00') as srv: 157 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 158 | auth = aiosocks.Socks5Auth('usr', 'pwd') 159 | dst = ('python.org', 80) 160 | 161 | with pytest.raises(aiosocks.SocksError) as ct: 162 | await aiosocks.create_connection( 163 | None, addr, auth, dst, loop=loop) 164 | assert 'invalid version' in str(ct.value) 165 | 166 | 167 | async def test_socks5_cmd_not_granted(loop): 168 | async with FakeSocksSrv(loop, b'\x05\x02\x01\x00\x05\x01\x00') as srv: 169 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 170 | auth = aiosocks.Socks5Auth('usr', 'pwd') 171 | dst = ('python.org', 80) 172 | 173 | with pytest.raises(aiosocks.SocksError) as ct: 174 | await aiosocks.create_connection( 175 | None, addr, auth, dst, loop=loop) 176 | assert 'General SOCKS server failure' in str(ct.value) 177 | 178 | 179 | async def test_socks5_invalid_address_type(loop): 180 | async with FakeSocksSrv(loop, b'\x05\x02\x01\x00\x05\x00\x00\xFF') as srv: 181 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 182 | auth = aiosocks.Socks5Auth('usr', 'pwd') 183 | dst = ('python.org', 80) 184 | 185 | with pytest.raises(aiosocks.SocksError) as ct: 186 | await aiosocks.create_connection( 187 | None, addr, auth, dst, loop=loop) 188 | assert 'invalid data' in str(ct.value) 189 | 190 | 191 | async def test_socks5_atype_ipv4(loop): 192 | pld = b'\x05\x02\x01\x00\x05\x00\x00\x01\x01\x01\x01\x01\x04W' 193 | 194 | async with FakeSocksSrv(loop, pld) as srv: 195 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 196 | auth = aiosocks.Socks5Auth('usr', 'pwd') 197 | dst = ('python.org', 80) 198 | 199 | transport, protocol = await aiosocks.create_connection( 200 | None, addr, auth, dst, loop=loop) 201 | assert protocol.proxy_sockname == ('1.1.1.1', 1111) 202 | 203 | transport.close() 204 | 205 | 206 | async def test_socks5_atype_ipv6(loop): 207 | pld = b'\x05\x02\x01\x00\x05\x00\x00\x04\x00\x00\x00\x00' \ 208 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x11\x04W' 209 | 210 | async with FakeSocksSrv(loop, pld) as srv: 211 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 212 | auth = aiosocks.Socks5Auth('usr', 'pwd') 213 | dst = ('python.org', 80) 214 | 215 | transport, protocol = await aiosocks.create_connection( 216 | None, addr, auth, dst, loop=loop) 217 | assert protocol.proxy_sockname == ('::111', 1111) 218 | 219 | transport.close() 220 | 221 | 222 | async def test_socks5_atype_domain(loop): 223 | pld = b'\x05\x02\x01\x00\x05\x00\x00\x03\x0apython.org\x04W' 224 | 225 | async with FakeSocksSrv(loop, pld) as srv: 226 | addr = aiosocks.Socks5Addr('127.0.0.1', srv.port) 227 | auth = aiosocks.Socks5Auth('usr', 'pwd') 228 | dst = ('python.org', 80) 229 | 230 | transport, protocol = await aiosocks.create_connection( 231 | None, addr, auth, dst, loop=loop) 232 | assert protocol.proxy_sockname == (b'python.org', 1111) 233 | 234 | transport.close() 235 | 236 | 237 | async def test_http_connect(loop): 238 | async def handler(request): 239 | return web.Response(text='Test message') 240 | 241 | async with RawTestServer(handler, host='127.0.0.1', loop=loop) as ws: 242 | async with FakeSocks4Srv(loop) as srv: 243 | conn = ProxyConnector(loop=loop, remote_resolve=False) 244 | 245 | async with aiohttp.ClientSession( 246 | connector=conn, loop=loop, 247 | request_class=ProxyClientRequest) as ses: 248 | proxy = 'socks4://127.0.0.1:{}'.format(srv.port) 249 | 250 | async with ses.get(ws.make_url('/'), proxy=proxy) as resp: 251 | assert resp.status == 200 252 | assert (await resp.text()) == 'Test message' 253 | 254 | 255 | async def test_https_connect(loop): 256 | async def handler(request): 257 | return web.Response(text='Test message') 258 | 259 | here = os.path.join(os.path.dirname(__file__), '..', 'tests') 260 | keyfile = os.path.join(here, 'sample.key') 261 | certfile = os.path.join(here, 'sample.crt') 262 | sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) 263 | sslcontext.load_cert_chain(certfile, keyfile) 264 | 265 | ws = RawTestServer(handler, scheme='https', host='127.0.0.1', loop=loop) 266 | await ws.start_server(loop=loop, ssl=sslcontext) 267 | 268 | v_fp = (b'0\x9a\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd' 269 | b'\xcb~7U\x14D@L' 270 | b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x8b') 271 | inv_fp = (b'0\x9d\xc9D\x83\xdc\x91\'\x88\x91\x11\xa1d\x97\xfd' 272 | b'\xcb~7U\x14D@L' 273 | b'\x11\xab\x99\xa8\xae\xb7\x14\xee\x9e') 274 | 275 | async with FakeSocks4Srv(loop) as srv: 276 | v_conn = ProxyConnector(loop=loop, remote_resolve=False, 277 | fingerprint=v_fp) 278 | inv_conn = ProxyConnector(loop=loop, remote_resolve=False, 279 | fingerprint=inv_fp) 280 | 281 | async with aiohttp.ClientSession( 282 | connector=v_conn, loop=loop, 283 | request_class=ProxyClientRequest) as ses: 284 | proxy = 'socks4://127.0.0.1:{}'.format(srv.port) 285 | 286 | async with ses.get(ws.make_url('/'), proxy=proxy) as resp: 287 | assert resp.status == 200 288 | assert (await resp.text()) == 'Test message' 289 | 290 | async with aiohttp.ClientSession( 291 | connector=inv_conn, loop=loop, 292 | request_class=ProxyClientRequest) as ses: 293 | proxy = 'socks4://127.0.0.1:{}'.format(srv.port) 294 | 295 | with pytest.raises(aiohttp.ServerFingerprintMismatch): 296 | async with ses.get(ws.make_url('/'), proxy=proxy) as resp: 297 | assert resp.status == 200 298 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import aiosocks 3 | 4 | 5 | def test_socks4_auth1(): 6 | with pytest.raises(ValueError): 7 | aiosocks.Socks4Auth(None) 8 | 9 | 10 | def test_socks4_auth2(): 11 | auth = aiosocks.Socks4Auth('usr', encoding='ascii') 12 | assert auth.login == b'usr' 13 | 14 | 15 | def test_socks4_auth3(): 16 | auth = aiosocks.Socks4Auth('usrё', encoding='utf-8') 17 | assert auth.login == b'usr\xd1\x91' 18 | 19 | 20 | def test_socks5_auth1(): 21 | with pytest.raises(ValueError): 22 | aiosocks.Socks5Auth(None, '') 23 | 24 | 25 | def test_socks5_auth2(): 26 | with pytest.raises(ValueError): 27 | aiosocks.Socks5Auth('', None) 28 | 29 | 30 | def test_socks5_auth3(): 31 | auth = aiosocks.Socks5Auth('usr', 'pwd', encoding='ascii') 32 | assert auth.login == b'usr' 33 | assert auth.password == b'pwd' 34 | 35 | 36 | def test_socks5_auth4(): 37 | auth = aiosocks.Socks5Auth('usrё', 'pwdё', encoding='utf-8') 38 | assert auth.login == b'usr\xd1\x91' 39 | assert auth.password == b'pwd\xd1\x91' 40 | 41 | 42 | def test_socks4_addr1(): 43 | with pytest.raises(ValueError): 44 | aiosocks.Socks4Addr(None) 45 | 46 | 47 | def test_socks4_addr2(): 48 | addr = aiosocks.Socks4Addr('localhost') 49 | assert addr.host == 'localhost' 50 | assert addr.port == 1080 51 | 52 | 53 | def test_socks4_addr3(): 54 | addr = aiosocks.Socks4Addr('localhost', 1) 55 | assert addr.host == 'localhost' 56 | assert addr.port == 1 57 | 58 | 59 | def test_socks4_addr4(): 60 | addr = aiosocks.Socks4Addr('localhost', None) 61 | assert addr.host == 'localhost' 62 | assert addr.port == 1080 63 | 64 | 65 | def test_socks5_addr1(): 66 | with pytest.raises(ValueError): 67 | aiosocks.Socks5Addr(None) 68 | 69 | 70 | def test_socks5_addr2(): 71 | addr = aiosocks.Socks5Addr('localhost') 72 | assert addr.host == 'localhost' 73 | assert addr.port == 1080 74 | 75 | 76 | def test_socks5_addr3(): 77 | addr = aiosocks.Socks5Addr('localhost', 1) 78 | assert addr.host == 'localhost' 79 | assert addr.port == 1 80 | 81 | 82 | def test_socks5_addr4(): 83 | addr = aiosocks.Socks5Addr('localhost', None) 84 | assert addr.host == 'localhost' 85 | assert addr.port == 1080 86 | -------------------------------------------------------------------------------- /tests/test_protocols.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiosocks 3 | import pytest 4 | import socket 5 | import ssl as ssllib 6 | from unittest import mock 7 | from asyncio import coroutine as coro, sslproto 8 | from aiohttp.test_utils import make_mocked_coro 9 | import aiosocks.constants as c 10 | from aiosocks.protocols import BaseSocksProtocol 11 | 12 | 13 | def make_base(loop, *, dst=None, waiter=None, ap_factory=None, ssl=None): 14 | dst = dst or ('python.org', 80) 15 | 16 | proto = BaseSocksProtocol(None, None, dst=dst, ssl=ssl, 17 | loop=loop, waiter=waiter, 18 | app_protocol_factory=ap_factory) 19 | return proto 20 | 21 | 22 | def make_socks4(loop, *, addr=None, auth=None, rr=True, dst=None, r=b'', 23 | ap_factory=None, whiter=None): 24 | addr = addr or aiosocks.Socks4Addr('localhost', 1080) 25 | auth = auth or aiosocks.Socks4Auth('user') 26 | dst = dst or ('python.org', 80) 27 | 28 | proto = aiosocks.Socks4Protocol( 29 | proxy=addr, proxy_auth=auth, dst=dst, remote_resolve=rr, 30 | loop=loop, app_protocol_factory=ap_factory, waiter=whiter) 31 | proto._stream_writer = mock.Mock() 32 | proto.read_response = mock.Mock( 33 | side_effect=coro(mock.Mock(return_value=r))) 34 | proto._get_dst_addr = mock.Mock( 35 | side_effect=coro(mock.Mock(return_value=(socket.AF_INET, '127.0.0.1'))) 36 | ) 37 | return proto 38 | 39 | 40 | def make_socks5(loop, *, addr=None, auth=None, rr=True, dst=None, r=None, 41 | ap_factory=None, whiter=None): 42 | addr = addr or aiosocks.Socks5Addr('localhost', 1080) 43 | auth = auth or aiosocks.Socks5Auth('user', 'pwd') 44 | dst = dst or ('python.org', 80) 45 | 46 | proto = aiosocks.Socks5Protocol( 47 | proxy=addr, proxy_auth=auth, dst=dst, remote_resolve=rr, 48 | loop=loop, app_protocol_factory=ap_factory, waiter=whiter) 49 | proto._stream_writer = mock.Mock() 50 | proto._stream_writer.drain = make_mocked_coro(True) 51 | 52 | if not isinstance(r, (list, tuple)): 53 | proto.read_response = mock.Mock( 54 | side_effect=coro(mock.Mock(return_value=r))) 55 | else: 56 | proto.read_response = mock.Mock( 57 | side_effect=coro(mock.Mock(side_effect=r))) 58 | 59 | proto._get_dst_addr = mock.Mock( 60 | side_effect=coro(mock.Mock(return_value=(socket.AF_INET, '127.0.0.1'))) 61 | ) 62 | return proto 63 | 64 | 65 | def test_base_ctor(loop): 66 | with pytest.raises(ValueError): 67 | BaseSocksProtocol(None, None, None, loop=loop, 68 | waiter=None, app_protocol_factory=None) 69 | 70 | with pytest.raises(ValueError): 71 | BaseSocksProtocol(None, None, 123, loop=loop, 72 | waiter=None, app_protocol_factory=None) 73 | 74 | with pytest.raises(ValueError): 75 | BaseSocksProtocol(None, None, ('python.org',), loop=loop, 76 | waiter=None, app_protocol_factory=None) 77 | 78 | 79 | def test_base_write_request(loop): 80 | proto = make_base(loop) 81 | proto._stream_writer = mock.Mock() 82 | 83 | proto.write_request([b'\x00', b'\x01\x02', 0x03]) 84 | proto._stream_writer.write.assert_called_with(b'\x00\x01\x02\x03') 85 | 86 | with pytest.raises(ValueError): 87 | proto.write_request(['\x00']) 88 | 89 | 90 | async def test_base_negotiate_os_error(loop): 91 | waiter = asyncio.Future(loop=loop) 92 | proto = make_base(loop, waiter=waiter) 93 | proto.socks_request = make_mocked_coro(raise_exception=OSError('test')) 94 | await proto.negotiate(None, None) 95 | 96 | with pytest.raises(OSError) as ct: 97 | await waiter 98 | assert 'test' in str(ct.value) 99 | 100 | 101 | async def test_base_negotiate_socks_err(loop): 102 | waiter = asyncio.Future(loop=loop) 103 | proto = make_base(loop, waiter=waiter) 104 | proto.socks_request = make_mocked_coro( 105 | raise_exception=aiosocks.SocksError('test')) 106 | await proto.negotiate(None, None) 107 | 108 | with pytest.raises(aiosocks.SocksError) as ct: 109 | await waiter 110 | assert 'Can not connect to' in str(ct.value) 111 | 112 | 113 | async def test_base_negotiate_without_app_proto(loop): 114 | waiter = asyncio.Future(loop=loop) 115 | proto = make_base(loop, waiter=waiter) 116 | proto.socks_request = make_mocked_coro((None, None)) 117 | proto._transport = True 118 | 119 | await proto.negotiate(None, None) 120 | await waiter 121 | assert waiter.done() 122 | 123 | 124 | async def test_base_negotiate_with_app_proto(loop): 125 | waiter = asyncio.Future(loop=loop) 126 | proto = make_base(loop, waiter=waiter, 127 | ap_factory=lambda: asyncio.Protocol()) 128 | proto.socks_request = make_mocked_coro((None, None)) 129 | 130 | await proto.negotiate(None, None) 131 | await waiter 132 | assert waiter.done() 133 | 134 | 135 | def test_base_connection_lost(): 136 | loop_mock = mock.Mock() 137 | app_proto = mock.Mock() 138 | 139 | proto = make_base(loop_mock, ap_factory=lambda: app_proto) 140 | 141 | # negotiate not completed 142 | proto._negotiate_done = False 143 | proto.connection_lost(True) 144 | assert not loop_mock.call_soon.called 145 | 146 | # negotiate successfully competed 147 | loop_mock.reset_mock() 148 | proto._negotiate_done = True 149 | proto.connection_lost(True) 150 | assert loop_mock.call_soon.called 151 | 152 | # don't call connect_lost, if app_protocol == self 153 | # otherwise recursion 154 | loop_mock.reset_mock() 155 | proto = make_base(loop_mock, ap_factory=None) 156 | proto._negotiate_done = True 157 | proto.connection_lost(True) 158 | assert not loop_mock.call_soon.called 159 | 160 | 161 | def test_base_pause_writing(): 162 | loop_mock = mock.Mock() 163 | app_proto = mock.Mock() 164 | 165 | proto = make_base(loop_mock, ap_factory=lambda: app_proto) 166 | 167 | # negotiate not completed 168 | proto._negotiate_done = False 169 | proto.pause_writing() 170 | assert not proto._app_protocol.pause_writing.called 171 | 172 | # negotiate successfully competed 173 | app_proto.reset_mock() 174 | proto._negotiate_done = True 175 | proto.pause_writing() 176 | assert proto._app_protocol.pause_writing.called 177 | 178 | # don't call pause_writing, if app_protocol == self 179 | # otherwise recursion 180 | app_proto.reset_mock() 181 | proto = make_base(loop_mock) 182 | proto._negotiate_done = True 183 | proto.pause_writing() 184 | 185 | 186 | def test_base_resume_writing(): 187 | loop_mock = mock.Mock() 188 | app_proto = mock.Mock() 189 | 190 | proto = make_base(loop_mock, ap_factory=lambda: app_proto) 191 | 192 | # negotiate not completed 193 | proto._negotiate_done = False 194 | # negotiate not completed 195 | with pytest.raises(AssertionError): 196 | proto.resume_writing() 197 | assert not proto._app_protocol.resume_writing.called 198 | 199 | # negotiate successfully competed 200 | loop_mock.reset_mock() 201 | proto._negotiate_done = True 202 | proto.resume_writing() 203 | assert proto._app_protocol.resume_writing.called 204 | 205 | # don't call resume_writing, if app_protocol == self 206 | # otherwise recursion 207 | loop_mock.reset_mock() 208 | proto = make_base(loop_mock) 209 | proto._negotiate_done = True 210 | with pytest.raises(AssertionError): 211 | proto.resume_writing() 212 | 213 | 214 | def test_base_data_received(): 215 | loop_mock = mock.Mock() 216 | app_proto = mock.Mock() 217 | 218 | proto = make_base(loop_mock, ap_factory=lambda: app_proto) 219 | 220 | # negotiate not completed 221 | proto._negotiate_done = False 222 | proto.data_received(b'123') 223 | assert not proto._app_protocol.data_received.called 224 | 225 | # negotiate successfully competed 226 | app_proto.reset_mock() 227 | proto._negotiate_done = True 228 | proto.data_received(b'123') 229 | assert proto._app_protocol.data_received.called 230 | 231 | # don't call data_received, if app_protocol == self 232 | # otherwise recursion 233 | loop_mock.reset_mock() 234 | proto = make_base(loop_mock) 235 | proto._negotiate_done = True 236 | proto.data_received(b'123') 237 | 238 | 239 | def test_base_eof_received(): 240 | loop_mock = mock.Mock() 241 | app_proto = mock.Mock() 242 | 243 | proto = make_base(loop_mock, ap_factory=lambda: app_proto) 244 | 245 | # negotiate not completed 246 | proto._negotiate_done = False 247 | proto.eof_received() 248 | assert not proto._app_protocol.eof_received.called 249 | 250 | # negotiate successfully competed 251 | app_proto.reset_mock() 252 | proto._negotiate_done = True 253 | proto.eof_received() 254 | assert proto._app_protocol.eof_received.called 255 | 256 | # don't call pause_writing, if app_protocol == self 257 | # otherwise recursion 258 | app_proto.reset_mock() 259 | proto = make_base(loop_mock) 260 | proto._negotiate_done = True 261 | proto.eof_received() 262 | 263 | 264 | async def test_base_make_ssl_proto(): 265 | loop_mock = mock.Mock() 266 | app_proto = mock.Mock() 267 | 268 | ssl_context = ssllib.create_default_context() 269 | proto = make_base(loop_mock, 270 | ap_factory=lambda: app_proto, ssl=ssl_context) 271 | proto.socks_request = make_mocked_coro((None, None)) 272 | proto._transport = mock.Mock() 273 | await proto.negotiate(None, None) 274 | 275 | assert isinstance(proto._transport, sslproto._SSLProtocolTransport) 276 | 277 | 278 | async def test_base_func_negotiate_cb_call(): 279 | loop_mock = mock.Mock() 280 | waiter = mock.Mock() 281 | 282 | proto = make_base(loop_mock, waiter=waiter) 283 | proto.socks_request = make_mocked_coro((None, None)) 284 | proto._negotiate_done_cb = mock.Mock() 285 | 286 | with mock.patch('aiosocks.protocols.asyncio.Task') as task_mock: 287 | await proto.negotiate(None, None) 288 | assert proto._negotiate_done_cb.called 289 | assert not task_mock.called 290 | 291 | 292 | async def test_base_coro_negotiate_cb_call(): 293 | loop_mock = mock.Mock() 294 | waiter = mock.Mock() 295 | 296 | proto = make_base(loop_mock, waiter=waiter) 297 | proto.socks_request = make_mocked_coro((None, None)) 298 | proto._negotiate_done_cb = make_mocked_coro(None) 299 | 300 | await (await proto.negotiate(None, None)) 301 | assert proto._negotiate_done_cb.called 302 | 303 | 304 | async def test_base_reader_limit(loop): 305 | proto = BaseSocksProtocol(None, None, ('python.org', 80), 306 | None, None, reader_limit=10, loop=loop) 307 | assert proto.reader._limit == 10 308 | 309 | proto = BaseSocksProtocol(None, None, ('python.org', 80), 310 | None, None, reader_limit=15, loop=loop) 311 | assert proto.reader._limit == 15 312 | 313 | 314 | async def test_base_incomplete_error(loop): 315 | proto = BaseSocksProtocol(None, None, ('python.org', 80), 316 | None, None, reader_limit=10, loop=loop) 317 | proto._stream_reader.readexactly = make_mocked_coro( 318 | raise_exception=asyncio.IncompleteReadError(b'part', 5)) 319 | with pytest.raises(aiosocks.InvalidServerReply): 320 | await proto.read_response(4) 321 | 322 | 323 | def test_socks4_ctor(loop): 324 | addr = aiosocks.Socks4Addr('localhost', 1080) 325 | auth = aiosocks.Socks4Auth('user') 326 | dst = ('python.org', 80) 327 | 328 | with pytest.raises(ValueError): 329 | aiosocks.Socks4Protocol(None, None, dst, loop=loop, 330 | waiter=None, app_protocol_factory=None) 331 | 332 | with pytest.raises(ValueError): 333 | aiosocks.Socks4Protocol(None, auth, dst, loop=loop, 334 | waiter=None, app_protocol_factory=None) 335 | 336 | with pytest.raises(ValueError): 337 | aiosocks.Socks4Protocol(aiosocks.Socks5Addr('host'), auth, dst, 338 | loop=loop, waiter=None, 339 | app_protocol_factory=None) 340 | 341 | with pytest.raises(ValueError): 342 | aiosocks.Socks4Protocol(addr, aiosocks.Socks5Auth('l', 'p'), dst, 343 | loop=loop, waiter=None, 344 | app_protocol_factory=None) 345 | 346 | aiosocks.Socks4Protocol(addr, None, dst, loop=loop, 347 | waiter=None, app_protocol_factory=None) 348 | aiosocks.Socks4Protocol(addr, auth, dst, loop=loop, 349 | waiter=None, app_protocol_factory=None) 350 | 351 | 352 | async def test_socks4_dst_domain_with_remote_resolve(loop): 353 | proto = make_socks4(loop, dst=('python.org', 80), 354 | r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 355 | 356 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 357 | proto._stream_writer.write.assert_called_with( 358 | b'\x04\x01\x00P\x00\x00\x00\x01user\x00python.org\x00') 359 | 360 | 361 | async def test_socks4_dst_domain_with_local_resolve(loop): 362 | proto = make_socks4(loop, dst=('python.org', 80), 363 | rr=False, r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 364 | 365 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 366 | proto._stream_writer.write.assert_called_with( 367 | b'\x04\x01\x00P\x7f\x00\x00\x01user\x00') 368 | 369 | 370 | async def test_socks4_dst_ip_with_remote_resolve(loop): 371 | proto = make_socks4(loop, dst=('127.0.0.1', 8800), 372 | r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 373 | 374 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 375 | proto._stream_writer.write.assert_called_with( 376 | b'\x04\x01"`\x7f\x00\x00\x01user\x00') 377 | 378 | 379 | async def test_socks4_dst_ip_with_locale_resolve(loop): 380 | proto = make_socks4(loop, dst=('127.0.0.1', 8800), 381 | rr=False, r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 382 | 383 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 384 | proto._stream_writer.write.assert_called_with( 385 | b'\x04\x01"`\x7f\x00\x00\x01user\x00') 386 | 387 | 388 | async def test_socks4_dst_domain_without_user(loop): 389 | proto = make_socks4(loop, auth=aiosocks.Socks4Auth(''), 390 | dst=('python.org', 80), 391 | r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 392 | 393 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 394 | proto._stream_writer.write.assert_called_with( 395 | b'\x04\x01\x00P\x00\x00\x00\x01\x00python.org\x00') 396 | 397 | 398 | async def test_socks4_dst_ip_without_user(loop): 399 | proto = make_socks4(loop, auth=aiosocks.Socks4Auth(''), 400 | dst=('127.0.0.1', 8800), 401 | r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 402 | 403 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 404 | proto._stream_writer.write.assert_called_with( 405 | b'\x04\x01"`\x7f\x00\x00\x01\x00') 406 | 407 | 408 | async def test_socks4_valid_resp_handling(loop): 409 | proto = make_socks4(loop, r=b'\x00\x5a\x00P\x7f\x00\x00\x01') 410 | 411 | r = await proto.socks_request(c.SOCKS_CMD_CONNECT) 412 | assert r == (('python.org', 80), ('127.0.0.1', 80)) 413 | 414 | 415 | async def test_socks4_invalid_reply_resp_handling(loop): 416 | proto = make_socks4(loop, r=b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF') 417 | 418 | with pytest.raises(aiosocks.InvalidServerReply): 419 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 420 | 421 | 422 | async def test_socks_err_resp_handling(loop): 423 | proto = make_socks4(loop, r=b'\x00\x5b\x00P\x7f\x00\x00\x01') 424 | 425 | with pytest.raises(aiosocks.SocksError) as cm: 426 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 427 | assert '0x5b' in str(cm.value) 428 | 429 | 430 | async def test_socks4_unknown_err_resp_handling(loop): 431 | proto = make_socks4(loop, r=b'\x00\x5e\x00P\x7f\x00\x00\x01') 432 | 433 | with pytest.raises(aiosocks.SocksError) as cm: 434 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 435 | assert 'Unknown error' in str(cm.value) 436 | 437 | 438 | def test_socks5_ctor(loop): 439 | addr = aiosocks.Socks5Addr('localhost', 1080) 440 | auth = aiosocks.Socks5Auth('user', 'pwd') 441 | dst = ('python.org', 80) 442 | 443 | with pytest.raises(ValueError): 444 | aiosocks.Socks5Protocol(None, None, dst, loop=loop, 445 | waiter=None, app_protocol_factory=None) 446 | 447 | with pytest.raises(ValueError): 448 | aiosocks.Socks5Protocol(None, auth, dst, loop=loop, 449 | waiter=None, app_protocol_factory=None) 450 | 451 | with pytest.raises(ValueError): 452 | aiosocks.Socks5Protocol(aiosocks.Socks4Addr('host'), 453 | auth, dst, loop=loop, 454 | waiter=None, app_protocol_factory=None) 455 | 456 | with pytest.raises(ValueError): 457 | aiosocks.Socks5Protocol(addr, aiosocks.Socks4Auth('l'), 458 | dst, loop=loop, 459 | waiter=None, app_protocol_factory=None) 460 | 461 | aiosocks.Socks5Protocol(addr, None, dst, loop=loop, 462 | waiter=None, app_protocol_factory=None) 463 | aiosocks.Socks5Protocol(addr, auth, dst, loop=loop, 464 | waiter=None, app_protocol_factory=None) 465 | 466 | 467 | async def test_socks5_auth_inv_srv_ver(loop): 468 | proto = make_socks5(loop, r=b'\x00\x00') 469 | 470 | with pytest.raises(aiosocks.InvalidServerVersion): 471 | await proto.authenticate() 472 | 473 | 474 | async def test_socks5_auth_no_acceptable_auth_methods(loop): 475 | proto = make_socks5(loop, r=b'\x05\xFF') 476 | 477 | with pytest.raises(aiosocks.NoAcceptableAuthMethods): 478 | await proto.authenticate() 479 | 480 | 481 | async def test_socks5_auth_unsupported_auth_method(loop): 482 | proto = make_socks5(loop, r=b'\x05\xF0') 483 | 484 | with pytest.raises(aiosocks.InvalidServerReply): 485 | await proto.authenticate() 486 | 487 | 488 | async def test_socks5_auth_usr_pwd_granted(loop): 489 | proto = make_socks5(loop, r=(b'\x05\x02', b'\x01\x00',)) 490 | await proto.authenticate() 491 | 492 | proto._stream_writer.write.assert_has_calls([ 493 | mock.call(b'\x05\x02\x00\x02'), 494 | mock.call(b'\x01\x04user\x03pwd') 495 | ]) 496 | 497 | 498 | async def test_socks5_auth_invalid_reply(loop): 499 | proto = make_socks5(loop, r=(b'\x05\x02', b'\x00\x00',)) 500 | 501 | with pytest.raises(aiosocks.InvalidServerReply): 502 | await proto.authenticate() 503 | 504 | 505 | async def test_socks5_auth_access_denied(loop): 506 | proto = make_socks5(loop, r=(b'\x05\x02', b'\x01\x01',)) 507 | 508 | with pytest.raises(aiosocks.LoginAuthenticationFailed): 509 | await proto.authenticate() 510 | 511 | 512 | async def test_socks5_auth_anonymous_granted(loop): 513 | proto = make_socks5(loop, r=b'\x05\x00') 514 | await proto.authenticate() 515 | 516 | 517 | async def test_socks5_build_dst_addr_ipv4(loop): 518 | proto = make_socks5(loop) 519 | dst_req, resolved = await proto.build_dst_address('127.0.0.1', 80) 520 | 521 | assert dst_req == [0x01, b'\x7f\x00\x00\x01', b'\x00P'] 522 | assert resolved == ('127.0.0.1', 80) 523 | 524 | 525 | async def test_socks5_build_dst_addr_ipv6(loop): 526 | proto = make_socks5(loop) 527 | dst_req, resolved = await proto.build_dst_address( 528 | '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', 80) 529 | 530 | assert dst_req == [ 531 | 0x04, b' \x01\r\xb8\x11\xa3\t\xd7\x1f4\x8a.\x07\xa0v]', b'\x00P'] 532 | assert resolved == ('2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d', 80) 533 | 534 | 535 | async def test_socks5_build_dst_addr_domain_with_remote_resolve(loop): 536 | proto = make_socks5(loop) 537 | dst_req, resolved = await proto.build_dst_address('python.org', 80) 538 | 539 | assert dst_req == [0x03, b'\n', b'python.org', b'\x00P'] 540 | assert resolved == ('python.org', 80) 541 | 542 | 543 | async def test_socks5_build_dst_addr_domain_with_locale_resolve(loop): 544 | proto = make_socks5(loop, rr=False) 545 | dst_req, resolved = await proto.build_dst_address('python.org', 80) 546 | 547 | assert dst_req == [0x01, b'\x7f\x00\x00\x01', b'\x00P'] 548 | assert resolved == ('127.0.0.1', 80) 549 | 550 | 551 | async def test_socks5_rd_addr_ipv4(loop): 552 | proto = make_socks5(loop, r=[b'\x01', b'\x7f\x00\x00\x01', b'\x00P']) 553 | r = await proto.read_address() 554 | 555 | assert r == ('127.0.0.1', 80) 556 | 557 | 558 | async def test_socks5_rd_addr_ipv6(loop): 559 | resp = [ 560 | b'\x04', 561 | b' \x01\r\xb8\x11\xa3\t\xd7\x1f4\x8a.\x07\xa0v]', 562 | b'\x00P' 563 | ] 564 | proto = make_socks5(loop, r=resp) 565 | r = await proto.read_address() 566 | 567 | assert r == ('2001:db8:11a3:9d7:1f34:8a2e:7a0:765d', 80) 568 | 569 | 570 | async def test_socks5_rd_addr_domain(loop): 571 | proto = make_socks5(loop, r=[b'\x03', b'\n', b'python.org', b'\x00P']) 572 | r = await proto.read_address() 573 | 574 | assert r == (b'python.org', 80) 575 | 576 | 577 | async def test_socks5_socks_req_inv_ver(loop): 578 | proto = make_socks5(loop, r=[b'\x05\x00', b'\x04\x00\x00']) 579 | 580 | with pytest.raises(aiosocks.InvalidServerVersion): 581 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 582 | 583 | 584 | async def test_socks5_socks_req_socks_srv_err(loop): 585 | proto = make_socks5(loop, r=[b'\x05\x00', b'\x05\x02\x00']) 586 | 587 | with pytest.raises(aiosocks.SocksError) as ct: 588 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 589 | assert 'Connection not allowed by ruleset' in str(ct.value) 590 | 591 | 592 | async def test_socks5_socks_req_unknown_err(loop): 593 | proto = make_socks5(loop, r=[b'\x05\x00', b'\x05\xFF\x00']) 594 | 595 | with pytest.raises(aiosocks.SocksError) as ct: 596 | await proto.socks_request(c.SOCKS_CMD_CONNECT) 597 | assert 'Unknown error' in str(ct.value) 598 | 599 | 600 | async def test_socks_req_cmd_granted(loop): 601 | # cmd granted 602 | resp = [b'\x05\x00', 603 | b'\x05\x00\x00', 604 | b'\x01', b'\x7f\x00\x00\x01', 605 | b'\x00P'] 606 | proto = make_socks5(loop, r=resp) 607 | r = await proto.socks_request(c.SOCKS_CMD_CONNECT) 608 | 609 | assert r == (('python.org', 80), ('127.0.0.1', 80)) 610 | proto._stream_writer.write.assert_has_calls([ 611 | mock.call(b'\x05\x02\x00\x02'), 612 | mock.call(b'\x05\x01\x00\x03\npython.org\x00P') 613 | ]) 614 | --------------------------------------------------------------------------------