tiny-proxy 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/MANIFEST.in +1 -0
  2. tiny_proxy-0.3.0/PKG-INFO +72 -0
  3. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/README.md +4 -3
  4. tiny_proxy-0.3.0/pyproject.toml +59 -0
  5. tiny_proxy-0.3.0/tests/config.py +61 -0
  6. tiny_proxy-0.3.0/tests/conftest.py +165 -0
  7. tiny_proxy-0.3.0/tests/http_app.py +32 -0
  8. tiny_proxy-0.3.0/tests/http_server.py +37 -0
  9. tiny_proxy-0.3.0/tests/proxy_server.py +172 -0
  10. tiny_proxy-0.3.0/tests/utils.py +56 -0
  11. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/__init__.py +1 -1
  12. tiny_proxy-0.3.0/tiny_proxy/_proxy/__init__.py +0 -0
  13. tiny_proxy-0.3.0/tiny_proxy.egg-info/PKG-INFO +72 -0
  14. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy.egg-info/SOURCES.txt +7 -1
  15. tiny_proxy-0.3.0/tiny_proxy.egg-info/requires.txt +1 -0
  16. tiny_proxy-0.2.1/PKG-INFO +0 -51
  17. tiny_proxy-0.2.1/pyproject.toml +0 -10
  18. tiny_proxy-0.2.1/setup.py +0 -45
  19. tiny_proxy-0.2.1/tiny_proxy.egg-info/PKG-INFO +0 -51
  20. tiny_proxy-0.2.1/tiny_proxy.egg-info/requires.txt +0 -1
  21. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/LICENSE.txt +0 -0
  22. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/setup.cfg +0 -0
  23. {tiny_proxy-0.2.1/tiny_proxy/_handlers → tiny_proxy-0.3.0/tests}/__init__.py +0 -0
  24. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tests/test_proxy.py +0 -0
  25. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_errors.py +0 -0
  26. {tiny_proxy-0.2.1/tiny_proxy/_proxy → tiny_proxy-0.3.0/tiny_proxy/_handlers}/__init__.py +0 -0
  27. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_handlers/base.py +0 -0
  28. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_handlers/http.py +0 -0
  29. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_handlers/socks4.py +0 -0
  30. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_handlers/socks5.py +0 -0
  31. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_proxy/abc.py +0 -0
  32. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_proxy/http.py +0 -0
  33. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_proxy/socks4.py +0 -0
  34. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_proxy/socks5.py +0 -0
  35. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_stream.py +0 -0
  36. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy/_tunnel.py +0 -0
  37. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy.egg-info/dependency_links.txt +0 -0
  38. {tiny_proxy-0.2.1 → tiny_proxy-0.3.0}/tiny_proxy.egg-info/top_level.txt +0 -0
@@ -1,2 +1,3 @@
1
1
  # Include the license file
2
2
  include LICENSE.txt
3
+ recursive-include tests *
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiny-proxy
3
+ Version: 0.3.0
4
+ Summary: Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP CONNECT)
5
+ Author-email: Roman Snegirev <snegiryev@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: homepage, https://github.com/romis2012/tiny-proxy
8
+ Project-URL: repository, https://github.com/romis2012/tiny-proxy
9
+ Keywords: socks,socks5,socks4,http,proxy,proxy server,asyncio,trio,anyio
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Operating System :: MacOS
22
+ Classifier: Operating System :: Microsoft
23
+ Classifier: Operating System :: POSIX :: Linux
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Intended Audience :: Developers
26
+ Classifier: Framework :: AnyIO
27
+ Classifier: Framework :: AsyncIO
28
+ Classifier: Framework :: Trio
29
+ Classifier: License :: OSI Approved :: Apache Software License
30
+ Requires-Python: >=3.8.0
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE.txt
33
+ Requires-Dist: anyio<5.0.0,>=3.6.1
34
+ Dynamic: license-file
35
+
36
+ ## tiny-proxy
37
+
38
+ [![CI](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml)
39
+ [![PyPI version](https://badge.fury.io/py/tiny-proxy.svg)](https://pypi.python.org/pypi/tiny-proxy)
40
+ [![versions](https://img.shields.io/pypi/pyversions/tiny-proxy.svg)](https://github.com/romis2012/tiny-proxy)
41
+ <!-- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy) -->
42
+
43
+ Simple proxy (SOCKS4(a), SOCKS5(h), HTTP CONNECT) server built with [anyio](https://github.com/agronholm/anyio).
44
+ It is used for testing [python-socks](https://github.com/romis2012/python-socks), [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
45
+
46
+ ## Requirements
47
+ - Python >= 3.8
48
+ - anyio>=3.6.1
49
+
50
+ ## Installation
51
+ ```
52
+ pip install tiny-proxy
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ import anyio
59
+
60
+ from tiny_proxy import Socks5ProxyHandler
61
+
62
+
63
+ async def main():
64
+ handler = Socks5ProxyHandler(username='user', password='password')
65
+ listener = await anyio.create_tcp_listener(local_host='127.0.0.1', local_port=1080)
66
+ await listener.serve(handler.handle)
67
+
68
+
69
+ if __name__ == '__main__':
70
+ anyio.run(main)
71
+ ```
72
+
@@ -1,14 +1,15 @@
1
1
  ## tiny-proxy
2
2
 
3
3
  [![CI](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml)
4
- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy)
5
4
  [![PyPI version](https://badge.fury.io/py/tiny-proxy.svg)](https://pypi.python.org/pypi/tiny-proxy)
5
+ [![versions](https://img.shields.io/pypi/pyversions/tiny-proxy.svg)](https://github.com/romis2012/tiny-proxy)
6
+ <!-- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy) -->
6
7
 
7
- Simple proxy (SOCKS4(a), SOCKS5(h), HTTP tunnel) server built with [anyio](https://github.com/agronholm/anyio).
8
+ Simple proxy (SOCKS4(a), SOCKS5(h), HTTP CONNECT) server built with [anyio](https://github.com/agronholm/anyio).
8
9
  It is used for testing [python-socks](https://github.com/romis2012/python-socks), [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
9
10
 
10
11
  ## Requirements
11
- - Python >= 3.7
12
+ - Python >= 3.8
12
13
  - anyio>=3.6.1
13
14
 
14
15
  ## Installation
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ['setuptools']
3
+ build-backend = 'setuptools.build_meta'
4
+
5
+ [project]
6
+ name = 'tiny-proxy'
7
+ license = { text = 'Apache-2.0' }
8
+ description = 'Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP CONNECT)'
9
+ readme = 'README.md'
10
+ authors = [{ name = 'Roman Snegirev', email = 'snegiryev@gmail.com' }]
11
+ keywords = [
12
+ 'socks',
13
+ 'socks5',
14
+ 'socks4',
15
+ 'http',
16
+ 'proxy',
17
+ 'proxy server',
18
+ 'asyncio',
19
+ 'trio',
20
+ 'anyio',
21
+ ]
22
+ requires-python = ">=3.8.0"
23
+ dependencies = ['anyio>=3.6.1,<5.0.0']
24
+ dynamic = ['version']
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Programming Language :: Python",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Programming Language :: Python :: 3.8",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Programming Language :: Python :: 3.14",
37
+ "Operating System :: MacOS",
38
+ "Operating System :: Microsoft",
39
+ "Operating System :: POSIX :: Linux",
40
+ "Topic :: Internet :: WWW/HTTP",
41
+ "Intended Audience :: Developers",
42
+ "Framework :: AnyIO",
43
+ "Framework :: AsyncIO",
44
+ "Framework :: Trio",
45
+ "License :: OSI Approved :: Apache Software License",
46
+ ]
47
+
48
+ [project.urls]
49
+ homepage = 'https://github.com/romis2012/tiny-proxy'
50
+ repository = 'https://github.com/romis2012/tiny-proxy'
51
+
52
+ [tool.setuptools.dynamic]
53
+ version = { attr = 'tiny_proxy.__version__' }
54
+
55
+ [tool.setuptools.packages.find]
56
+ include = ['tiny_proxy*']
57
+
58
+ [tool.pytest.ini_options]
59
+ asyncio_mode = 'strict'
@@ -0,0 +1,61 @@
1
+ TEST_HTTP_HOST_IPV4 = '127.0.0.1'
2
+ TEST_HTTP_PORT_IPV4 = 8881
3
+ TEST_HTTP_URL_IPV4 = f'http://{TEST_HTTP_HOST_IPV4}:{TEST_HTTP_PORT_IPV4}/'
4
+
5
+ TEST_HTTPS_HOST_IPV4 = '127.0.0.1'
6
+ TEST_HTTPS_PORT_IPV4 = 8882
7
+ TEST_HTTPS_URL_IPV4 = f'https://{TEST_HTTPS_HOST_IPV4}:{TEST_HTTPS_PORT_IPV4}/'
8
+
9
+ TEST_HTTPS_HOST_IPV6 = '::1'
10
+ TEST_HTTPS_PORT_IPV6 = 8883
11
+ TEST_HTTPS_URL_IPV6 = f'https://[{TEST_HTTPS_HOST_IPV6}]:{TEST_HTTPS_PORT_IPV6}/'
12
+
13
+ PROXY_USERNAME = 'username'
14
+ PROXY_PASSWORD = 'password'
15
+
16
+ PROXY_HOST = '127.0.0.1'
17
+
18
+ SOCKS5_PROXY_PORT = 7780
19
+ SOCKS5_PROXY_PORT_NO_AUTH = 7781
20
+
21
+ SOCKS4_PROXY_PORT = 7782
22
+ SOCKS4_PROXY_PORT_NO_AUTH = 7783
23
+
24
+ HTTP_PROXY_PORT = 7784
25
+ HTTP_PROXY_PORT_NO_AUTH = 7785
26
+
27
+ SOCKS5_PROXY_URL = 'socks5://{username}:{password}@{host}:{port}'.format(
28
+ host=PROXY_HOST,
29
+ port=SOCKS5_PROXY_PORT,
30
+ username=PROXY_USERNAME,
31
+ password=PROXY_PASSWORD,
32
+ )
33
+
34
+ SOCKS5_PROXY_URL_NO_AUTH = 'socks5://{host}:{port}'.format(
35
+ host=PROXY_HOST,
36
+ port=SOCKS5_PROXY_PORT_NO_AUTH,
37
+ )
38
+
39
+ SOCKS4_PROXY_URL = 'socks4://{username}:{password}@{host}:{port}'.format(
40
+ host=PROXY_HOST,
41
+ port=SOCKS4_PROXY_PORT,
42
+ username=PROXY_USERNAME,
43
+ password='',
44
+ )
45
+
46
+ SOCKS4_PROXY_URL_NO_AUTH = 'socks4://{host}:{port}'.format(
47
+ host=PROXY_HOST,
48
+ port=SOCKS4_PROXY_PORT_NO_AUTH,
49
+ )
50
+
51
+ HTTP_PROXY_URL = 'http://{username}:{password}@{host}:{port}'.format(
52
+ host=PROXY_HOST,
53
+ port=HTTP_PROXY_PORT,
54
+ username=PROXY_USERNAME,
55
+ password=PROXY_PASSWORD,
56
+ )
57
+
58
+ HTTP_PROXY_URL_NO_AUTH = 'http://{host}:{port}'.format(
59
+ host=PROXY_HOST,
60
+ port=HTTP_PROXY_PORT_NO_AUTH,
61
+ )
@@ -0,0 +1,165 @@
1
+ import ssl
2
+
3
+ import pytest
4
+ import trustme
5
+
6
+ from tests.config import (
7
+ TEST_HTTP_HOST_IPV4,
8
+ TEST_HTTP_PORT_IPV4,
9
+ PROXY_HOST,
10
+ SOCKS5_PROXY_PORT,
11
+ PROXY_USERNAME,
12
+ PROXY_PASSWORD,
13
+ SOCKS5_PROXY_PORT_NO_AUTH,
14
+ SOCKS4_PROXY_PORT,
15
+ SOCKS4_PROXY_PORT_NO_AUTH,
16
+ HTTP_PROXY_PORT,
17
+ HTTP_PROXY_PORT_NO_AUTH,
18
+ TEST_HTTPS_HOST_IPV4,
19
+ TEST_HTTPS_PORT_IPV4,
20
+ TEST_HTTPS_HOST_IPV6,
21
+ TEST_HTTPS_PORT_IPV6,
22
+ )
23
+ from tests.http_server import HttpServerConfig, HttpServer
24
+ from tests.proxy_server import ProxyConfig, ProxyServerRunner
25
+ from tests.utils import wait_until_connectable
26
+
27
+
28
+ @pytest.fixture(scope='session')
29
+ def ssl_ca() -> trustme.CA:
30
+ return trustme.CA()
31
+
32
+
33
+ @pytest.fixture(scope='session')
34
+ def ssl_cert(ssl_ca: trustme.CA) -> trustme.LeafCert:
35
+ return ssl_ca.issue_cert(
36
+ "localhost",
37
+ "127.0.0.1",
38
+ "::1",
39
+ )
40
+
41
+
42
+ @pytest.fixture(scope='session')
43
+ def ssl_certfile(ssl_cert: trustme.LeafCert):
44
+ with ssl_cert.cert_chain_pems[0].tempfile() as cert_path:
45
+ yield cert_path
46
+
47
+
48
+ @pytest.fixture(scope='session')
49
+ def ssl_keyfile(ssl_cert: trustme.LeafCert):
50
+ with ssl_cert.private_key_pem.tempfile() as private_key_path:
51
+ yield private_key_path
52
+
53
+
54
+ @pytest.fixture(scope='session')
55
+ def ssl_key_and_cert_chain_file(ssl_cert: trustme.LeafCert):
56
+ with ssl_cert.private_key_and_cert_chain_pem.tempfile() as path:
57
+ yield path
58
+
59
+
60
+ @pytest.fixture(scope='session')
61
+ def ssl_ca_cert_file(ssl_ca: trustme.CA):
62
+ with ssl_ca.cert_pem.tempfile() as ca_cert_pem:
63
+ yield ca_cert_pem
64
+
65
+
66
+ @pytest.fixture(scope='session')
67
+ def server_ssl_context(ssl_cert: trustme.LeafCert) -> ssl.SSLContext:
68
+ ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
69
+ ssl_cert.configure_cert(ssl_ctx)
70
+ return ssl_ctx
71
+
72
+
73
+ @pytest.fixture(scope='session')
74
+ def client_ssl_context(ssl_ca: trustme.CA, ssl_ca_cert_file) -> ssl.SSLContext:
75
+ ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
76
+ ssl_ctx.verify_mode = ssl.CERT_REQUIRED
77
+ ssl_ctx.check_hostname = True
78
+
79
+ # ssl_ctx.load_verify_locations(ssl_ca_cert_file)
80
+ ssl_ca.configure_trust(ssl_ctx)
81
+
82
+ return ssl_ctx
83
+
84
+
85
+ @pytest.fixture(scope='session', autouse=True)
86
+ def web_server(ssl_certfile, ssl_keyfile):
87
+ config = [
88
+ HttpServerConfig(
89
+ host=TEST_HTTP_HOST_IPV4,
90
+ port=TEST_HTTP_PORT_IPV4,
91
+ ),
92
+ HttpServerConfig(
93
+ host=TEST_HTTPS_HOST_IPV4,
94
+ port=TEST_HTTPS_PORT_IPV4,
95
+ ssl_certfile=ssl_certfile,
96
+ ssl_keyfile=ssl_keyfile,
97
+ ),
98
+ HttpServerConfig(
99
+ host=TEST_HTTPS_HOST_IPV6,
100
+ port=TEST_HTTPS_PORT_IPV6,
101
+ ssl_certfile=ssl_certfile,
102
+ ssl_keyfile=ssl_keyfile,
103
+ ),
104
+ ]
105
+
106
+ server = HttpServer(config=config)
107
+ server.run()
108
+
109
+ for cfg in config:
110
+ wait_until_connectable(host=cfg.host, port=cfg.port)
111
+
112
+ yield None
113
+
114
+ server.shutdown()
115
+
116
+
117
+ @pytest.fixture(scope='session', autouse=True)
118
+ def proxy_server():
119
+ config = [
120
+ ProxyConfig(
121
+ proxy_type='socks5',
122
+ host=PROXY_HOST,
123
+ port=SOCKS5_PROXY_PORT,
124
+ username=PROXY_USERNAME,
125
+ password=PROXY_PASSWORD,
126
+ ),
127
+ ProxyConfig(
128
+ proxy_type='socks5',
129
+ host=PROXY_HOST,
130
+ port=SOCKS5_PROXY_PORT_NO_AUTH,
131
+ ),
132
+ ProxyConfig(
133
+ proxy_type='socks4',
134
+ host=PROXY_HOST,
135
+ port=SOCKS4_PROXY_PORT,
136
+ username=PROXY_USERNAME,
137
+ password=None,
138
+ ),
139
+ ProxyConfig(
140
+ proxy_type='socks4',
141
+ host=PROXY_HOST,
142
+ port=SOCKS4_PROXY_PORT_NO_AUTH,
143
+ ),
144
+ ProxyConfig(
145
+ proxy_type='http',
146
+ host=PROXY_HOST,
147
+ port=HTTP_PROXY_PORT,
148
+ username=PROXY_USERNAME,
149
+ password=PROXY_PASSWORD,
150
+ ),
151
+ ProxyConfig(
152
+ proxy_type='http',
153
+ host=PROXY_HOST,
154
+ port=HTTP_PROXY_PORT_NO_AUTH,
155
+ ),
156
+ ]
157
+
158
+ server = ProxyServerRunner(config=config)
159
+ server.run()
160
+ for cfg in config:
161
+ wait_until_connectable(host=cfg.host, port=cfg.port)
162
+
163
+ yield None
164
+
165
+ server.shutdown()
@@ -0,0 +1,32 @@
1
+ import ssl
2
+
3
+ from aiohttp import web
4
+
5
+
6
+ async def index(_):
7
+ return web.Response(body='Index')
8
+
9
+
10
+ app = web.Application()
11
+ app.router.add_get('/', index)
12
+
13
+
14
+ def run_app(
15
+ host: str,
16
+ port: int,
17
+ ssl_certfile: str = None,
18
+ ssl_keyfile: str = None,
19
+ ):
20
+ if ssl_certfile and ssl_keyfile:
21
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
22
+ ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile)
23
+ else:
24
+ ssl_context = None
25
+
26
+ web.run_app(
27
+ app,
28
+ host=host,
29
+ port=port,
30
+ ssl_context=ssl_context,
31
+ print=False, # type: ignore
32
+ )
@@ -0,0 +1,37 @@
1
+ import typing
2
+ from multiprocessing import Process
3
+
4
+ from tests.http_app import run_app
5
+
6
+
7
+ class HttpServerConfig(typing.NamedTuple):
8
+ host: str
9
+ port: int
10
+ ssl_certfile: str = None
11
+ ssl_keyfile: str = None
12
+
13
+ def to_dict(self):
14
+ d = {}
15
+ for key, val in self._asdict().items():
16
+ if val is not None:
17
+ d[key] = val
18
+ return d
19
+
20
+
21
+ class HttpServer:
22
+ def __init__(self, config: typing.Iterable[HttpServerConfig]):
23
+ self.config = config
24
+ self.workers = []
25
+
26
+ def run(self):
27
+ for cfg in self.config:
28
+ print(f'Starting web server on {cfg.host}:{cfg.port}...')
29
+ p = Process(target=run_app, kwargs=cfg.to_dict())
30
+ self.workers.append(p)
31
+
32
+ for p in self.workers:
33
+ p.start()
34
+
35
+ def shutdown(self):
36
+ for p in self.workers:
37
+ p.terminate()
@@ -0,0 +1,172 @@
1
+ import asyncio
2
+ import logging
3
+ import ssl
4
+ import typing
5
+ from contextlib import contextmanager
6
+ from multiprocessing import Process
7
+
8
+ from anyio import create_tcp_listener
9
+ from anyio.streams.tls import TLSListener
10
+
11
+ from tests.utils import cancel_all_tasks, cancel_tasks, wait_until_connectable
12
+ from tiny_proxy import HttpProxyHandler, Socks5ProxyHandler, Socks4ProxyHandler
13
+
14
+
15
+ class ProxyConfig(typing.NamedTuple):
16
+ proxy_type: str
17
+ host: str
18
+ port: int
19
+ username: typing.Optional[str] = None
20
+ password: typing.Optional[str] = None
21
+ ssl_certfile: typing.Optional[str] = None
22
+ ssl_keyfile: typing.Optional[str] = None
23
+
24
+ def to_dict(self):
25
+ d = {}
26
+ for key, val in self._asdict().items():
27
+ if val is not None:
28
+ d[key] = val
29
+ return d
30
+
31
+
32
+ class ProxyServer:
33
+ cls_map = {
34
+ 'http': HttpProxyHandler,
35
+ 'socks4': Socks4ProxyHandler,
36
+ 'socks5': Socks5ProxyHandler,
37
+ }
38
+
39
+ def __init__(self, config: typing.Iterable[ProxyConfig], loop: asyncio.AbstractEventLoop):
40
+ self.loop = loop
41
+ self.config = config
42
+ self.logger = logging.getLogger(__name__)
43
+ self.server_tasks = []
44
+
45
+ def run(self):
46
+ proxies = self.config
47
+ for proxy in proxies:
48
+ server_task = self.loop.create_task(self._listen(**proxy.to_dict()))
49
+ self.server_tasks.append(server_task)
50
+
51
+ def run_forever(self):
52
+ self.run()
53
+ self.loop.run_forever()
54
+
55
+ def shutdown(self):
56
+ print('Shutting down...')
57
+
58
+ cancel_tasks(self.server_tasks, self.loop)
59
+ cancel_all_tasks(self.loop)
60
+
61
+ self.loop.run_until_complete(self.loop.shutdown_asyncgens())
62
+ try:
63
+ self.loop.run_until_complete(self.loop.shutdown_default_executor())
64
+ except AttributeError: # pragma: no cover
65
+ pass # shutdown_default_executor is new to Python 3.9
66
+
67
+ self.loop.close()
68
+
69
+ def __enter__(self):
70
+ return self
71
+
72
+ def __exit__(self, exc_type, exc_val, exc_tb):
73
+ return self.shutdown()
74
+
75
+ async def _listen(
76
+ self,
77
+ proxy_type,
78
+ host,
79
+ port,
80
+ ssl_certfile=None,
81
+ ssl_keyfile=None,
82
+ **kwargs,
83
+ ):
84
+ handler_cls = self.cls_map.get(proxy_type)
85
+ if not handler_cls:
86
+ raise RuntimeError(f'Unsupported type: {proxy_type}')
87
+
88
+ if ssl_certfile and ssl_keyfile:
89
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
90
+ ssl_context.load_cert_chain(ssl_certfile, ssl_keyfile)
91
+ else:
92
+ ssl_context = None
93
+
94
+ print(f'Starting {proxy_type} proxy on {host}:{port}...')
95
+
96
+ handler = handler_cls(**kwargs)
97
+
98
+ listener = await create_tcp_listener(local_host=host, local_port=port)
99
+ if ssl_context is not None:
100
+ listener = TLSListener(listener=listener, ssl_context=ssl_context)
101
+
102
+ async with listener:
103
+ await listener.serve(handler.handle)
104
+
105
+
106
+ def _start_proxy_server(config: typing.Iterable[ProxyConfig]):
107
+ import asyncio
108
+
109
+ loop = asyncio.new_event_loop()
110
+ asyncio.set_event_loop(loop)
111
+
112
+ server = ProxyServer(config=config, loop=loop)
113
+
114
+ try:
115
+ server.run_forever()
116
+ except (KeyboardInterrupt, SystemExit):
117
+ pass
118
+ finally:
119
+ server.shutdown()
120
+
121
+
122
+ class ProxyServerRunner:
123
+ def __init__(self, config: typing.Iterable[ProxyConfig]):
124
+ self.config = config
125
+ self.process = None
126
+
127
+ def run(self):
128
+ """
129
+ https://pytest-cov.readthedocs.io/en/latest/subprocess-support.html#if-you-use-multiprocessing-process
130
+ or use Thread
131
+ """
132
+ try:
133
+ from pytest_cov.embed import cleanup_on_sigterm # noqa
134
+ except ImportError:
135
+ pass
136
+ else:
137
+ cleanup_on_sigterm()
138
+
139
+ self.process = Process(target=_start_proxy_server, kwargs=dict(config=self.config))
140
+ self.process.daemon = True
141
+ self.process.start()
142
+
143
+ def shutdown(self):
144
+ self.process.terminate()
145
+
146
+
147
+ @contextmanager
148
+ def start_proxy_server(config: ProxyConfig):
149
+ """
150
+ https://pytest-cov.readthedocs.io/en/latest/subprocess-support.html#if-you-use-multiprocessing-process
151
+ or use Thread
152
+ """
153
+ try:
154
+ from pytest_cov.embed import cleanup_on_sigterm # noqa
155
+ except ImportError:
156
+ pass
157
+ else:
158
+ cleanup_on_sigterm()
159
+
160
+ process = Process(target=_start_proxy_server, kwargs=dict(config=[config]))
161
+ # process = Thread(target=_start_proxy_server, kwargs=dict(config=[config]))
162
+ process.daemon = True
163
+ process.start()
164
+
165
+ wait_until_connectable(host=config.host, port=config.port)
166
+
167
+ try:
168
+ yield None
169
+ finally:
170
+ process.terminate()
171
+ process.join()
172
+ pass
@@ -0,0 +1,56 @@
1
+ import asyncio
2
+ import socket
3
+ import time
4
+ from typing import Iterable
5
+
6
+
7
+ def is_connectable(host, port):
8
+ sock = None
9
+ try:
10
+ sock = socket.create_connection((host, port), timeout=1)
11
+ except socket.error:
12
+ return False
13
+ else:
14
+ return True
15
+ finally:
16
+ if sock is not None:
17
+ sock.close()
18
+
19
+
20
+ def wait_until_connectable(host, port, timeout=10):
21
+ count = 0
22
+ while not is_connectable(host=host, port=port):
23
+ if count >= timeout:
24
+ raise Exception(
25
+ f'The proxy server has not available by ({host}, {port}) in {timeout:d} seconds'
26
+ )
27
+ count += 1
28
+ time.sleep(1)
29
+ return True
30
+
31
+
32
+ def cancel_tasks(tasks: Iterable[asyncio.Task], loop: asyncio.AbstractEventLoop):
33
+ if not tasks:
34
+ return
35
+
36
+ for task in tasks:
37
+ task.cancel()
38
+
39
+ loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
40
+
41
+ for task in tasks:
42
+ if task.cancelled():
43
+ continue
44
+ if task.exception() is not None:
45
+ loop.call_exception_handler(
46
+ {
47
+ "message": "unhandled exception during asyncio.run() shutdown",
48
+ "exception": task.exception(),
49
+ "task": task,
50
+ }
51
+ )
52
+
53
+
54
+ def cancel_all_tasks(loop: asyncio.AbstractEventLoop):
55
+ tasks = [task for task in asyncio.all_tasks(loop) if not task.done()]
56
+ cancel_tasks(tasks=tasks, loop=loop)
@@ -11,7 +11,7 @@ from ._handlers.http import HttpProxyHandler
11
11
  from ._handlers.socks4 import Socks4ProxyHandler
12
12
  from ._handlers.socks5 import Socks5ProxyHandler
13
13
 
14
- __version__ = '0.2.1'
14
+ __version__ = '0.3.0'
15
15
 
16
16
  __all__ = (
17
17
  'ProxyError',
File without changes
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiny-proxy
3
+ Version: 0.3.0
4
+ Summary: Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP CONNECT)
5
+ Author-email: Roman Snegirev <snegiryev@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: homepage, https://github.com/romis2012/tiny-proxy
8
+ Project-URL: repository, https://github.com/romis2012/tiny-proxy
9
+ Keywords: socks,socks5,socks4,http,proxy,proxy server,asyncio,trio,anyio
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Operating System :: MacOS
22
+ Classifier: Operating System :: Microsoft
23
+ Classifier: Operating System :: POSIX :: Linux
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Intended Audience :: Developers
26
+ Classifier: Framework :: AnyIO
27
+ Classifier: Framework :: AsyncIO
28
+ Classifier: Framework :: Trio
29
+ Classifier: License :: OSI Approved :: Apache Software License
30
+ Requires-Python: >=3.8.0
31
+ Description-Content-Type: text/markdown
32
+ License-File: LICENSE.txt
33
+ Requires-Dist: anyio<5.0.0,>=3.6.1
34
+ Dynamic: license-file
35
+
36
+ ## tiny-proxy
37
+
38
+ [![CI](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml)
39
+ [![PyPI version](https://badge.fury.io/py/tiny-proxy.svg)](https://pypi.python.org/pypi/tiny-proxy)
40
+ [![versions](https://img.shields.io/pypi/pyversions/tiny-proxy.svg)](https://github.com/romis2012/tiny-proxy)
41
+ <!-- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy) -->
42
+
43
+ Simple proxy (SOCKS4(a), SOCKS5(h), HTTP CONNECT) server built with [anyio](https://github.com/agronholm/anyio).
44
+ It is used for testing [python-socks](https://github.com/romis2012/python-socks), [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
45
+
46
+ ## Requirements
47
+ - Python >= 3.8
48
+ - anyio>=3.6.1
49
+
50
+ ## Installation
51
+ ```
52
+ pip install tiny-proxy
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ```python
58
+ import anyio
59
+
60
+ from tiny_proxy import Socks5ProxyHandler
61
+
62
+
63
+ async def main():
64
+ handler = Socks5ProxyHandler(username='user', password='password')
65
+ listener = await anyio.create_tcp_listener(local_host='127.0.0.1', local_port=1080)
66
+ await listener.serve(handler.handle)
67
+
68
+
69
+ if __name__ == '__main__':
70
+ anyio.run(main)
71
+ ```
72
+
@@ -2,8 +2,14 @@ LICENSE.txt
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
- setup.py
5
+ tests/__init__.py
6
+ tests/config.py
7
+ tests/conftest.py
8
+ tests/http_app.py
9
+ tests/http_server.py
10
+ tests/proxy_server.py
6
11
  tests/test_proxy.py
12
+ tests/utils.py
7
13
  tiny_proxy/__init__.py
8
14
  tiny_proxy/_errors.py
9
15
  tiny_proxy/_stream.py
@@ -0,0 +1 @@
1
+ anyio<5.0.0,>=3.6.1
tiny_proxy-0.2.1/PKG-INFO DELETED
@@ -1,51 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tiny_proxy
3
- Version: 0.2.1
4
- Summary: Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP tunnel)
5
- Home-page: https://github.com/romis2012/tiny-proxy
6
- Author: Roman Snegirev
7
- Author-email: snegiryev@gmail.com
8
- License: Apache 2
9
- Keywords: socks socks5 socks4 http proxy server asyncio trio anyio
10
- Platform: UNKNOWN
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE.txt
13
-
14
- ## tiny-proxy
15
-
16
- [![CI](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml)
17
- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy)
18
- [![PyPI version](https://badge.fury.io/py/tiny-proxy.svg)](https://pypi.python.org/pypi/tiny-proxy)
19
-
20
- Simple proxy (SOCKS4(a), SOCKS5(h), HTTP tunnel) server built with [anyio](https://github.com/agronholm/anyio).
21
- It is used for testing [python-socks](https://github.com/romis2012/python-socks), [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
22
-
23
- ## Requirements
24
- - Python >= 3.7
25
- - anyio>=3.6.1
26
-
27
- ## Installation
28
- ```
29
- pip install tiny-proxy
30
- ```
31
-
32
- ## Usage
33
-
34
- ```python
35
- import anyio
36
-
37
- from tiny_proxy import Socks5ProxyHandler
38
-
39
-
40
- async def main():
41
- handler = Socks5ProxyHandler(username='user', password='password')
42
- listener = await anyio.create_tcp_listener(local_host='127.0.0.1', local_port=1080)
43
- await listener.serve(handler.handle)
44
-
45
-
46
- if __name__ == '__main__':
47
- anyio.run(main)
48
- ```
49
-
50
-
51
-
@@ -1,10 +0,0 @@
1
- [tool.black]
2
- line-length = 99
3
- target-version = ['py37', 'py38', 'py39']
4
- skip-string-normalization = true
5
- # experimental-string-processing = true
6
- preview = true
7
- verbose = true
8
-
9
- [tool.pytest.ini_options]
10
- asyncio_mode = 'strict'
tiny_proxy-0.2.1/setup.py DELETED
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env python
2
- import os
3
- import re
4
- import sys
5
-
6
- from setuptools import setup
7
-
8
-
9
- def get_version():
10
- here = os.path.dirname(os.path.abspath(__file__))
11
- filename = os.path.join(here, 'tiny_proxy', '__init__.py')
12
- contents = open(filename).read()
13
- pattern = r"^__version__ = '(.*?)'$"
14
- return re.search(pattern, contents, re.MULTILINE).group(1)
15
-
16
-
17
- def get_long_description():
18
- with open('README.md', mode='r', encoding='utf8') as f:
19
- return f.read()
20
-
21
-
22
- if sys.version_info < (3, 7):
23
- raise RuntimeError('tiny-proxy requires Python 3.7+')
24
-
25
-
26
- setup(
27
- name='tiny_proxy',
28
- author='Roman Snegirev',
29
- author_email='snegiryev@gmail.com',
30
- version=get_version(),
31
- license='Apache 2',
32
- url='https://github.com/romis2012/tiny-proxy',
33
- description='Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP tunnel)',
34
- long_description=get_long_description(),
35
- long_description_content_type='text/markdown',
36
- packages=[
37
- 'tiny_proxy',
38
- 'tiny_proxy._proxy',
39
- 'tiny_proxy._handlers',
40
- ],
41
- keywords='socks socks5 socks4 http proxy server asyncio trio anyio',
42
- install_requires=[
43
- 'anyio>=3.6.1,<5.0',
44
- ],
45
- )
@@ -1,51 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tiny-proxy
3
- Version: 0.2.1
4
- Summary: Simple proxy server (SOCKS4(a), SOCKS5(h), HTTP tunnel)
5
- Home-page: https://github.com/romis2012/tiny-proxy
6
- Author: Roman Snegirev
7
- Author-email: snegiryev@gmail.com
8
- License: Apache 2
9
- Keywords: socks socks5 socks4 http proxy server asyncio trio anyio
10
- Platform: UNKNOWN
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE.txt
13
-
14
- ## tiny-proxy
15
-
16
- [![CI](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/tiny-proxy/actions/workflows/ci.yml)
17
- [![Coverage Status](https://codecov.io/gh/romis2012/tiny-proxy/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/tiny-proxy)
18
- [![PyPI version](https://badge.fury.io/py/tiny-proxy.svg)](https://pypi.python.org/pypi/tiny-proxy)
19
-
20
- Simple proxy (SOCKS4(a), SOCKS5(h), HTTP tunnel) server built with [anyio](https://github.com/agronholm/anyio).
21
- It is used for testing [python-socks](https://github.com/romis2012/python-socks), [aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
22
-
23
- ## Requirements
24
- - Python >= 3.7
25
- - anyio>=3.6.1
26
-
27
- ## Installation
28
- ```
29
- pip install tiny-proxy
30
- ```
31
-
32
- ## Usage
33
-
34
- ```python
35
- import anyio
36
-
37
- from tiny_proxy import Socks5ProxyHandler
38
-
39
-
40
- async def main():
41
- handler = Socks5ProxyHandler(username='user', password='password')
42
- listener = await anyio.create_tcp_listener(local_host='127.0.0.1', local_port=1080)
43
- await listener.serve(handler.handle)
44
-
45
-
46
- if __name__ == '__main__':
47
- anyio.run(main)
48
- ```
49
-
50
-
51
-
@@ -1 +0,0 @@
1
- anyio<5.0,>=3.6.1
File without changes
File without changes