Compare commits

...

13 Commits

Author SHA1 Message Date
Daniel
eb8d43841c fix: 修复代理状态检测不正确 2026-03-24 19:38:44 +08:00
Daniel
6a68d5b66a feat: add 新增验证码登录 2026-03-24 14:14:36 +08:00
Daniel
aaf46207a4 fix:add new file 2026-03-24 10:51:05 +08:00
Daniel
d5f1b2ae77 Revert "fix: 优化登录项"
This reverts commit 3b3fac1cee.
2026-03-16 23:05:07 +08:00
Daniel
1a339d82cd fix:检查版本升级导致的环境报错 2026-03-16 23:05:01 +08:00
Daniel
29c473890a 优化内容:代理设置 2026-03-16 22:24:02 +08:00
丹尼尔
3b3fac1cee fix: 优化登录项 2026-03-15 19:17:12 +08:00
丹尼尔
9f0e2a2db2 fix: 修复实时消息报错内容 2026-03-15 18:11:31 +08:00
丹尼尔
31cac56fad fix: 修复生产环境 2026-03-15 17:59:56 +08:00
丹尼尔
0e9bdf0052 fix: 修复生产环境 2026-03-15 17:41:16 +08:00
丹尼尔
19d8ad3721 fix: bug 2026-03-15 17:32:10 +08:00
丹尼尔
15c9e1772a fix: 修复代理问题 2026-03-15 17:16:05 +08:00
丹尼尔
8b62c445fc fix:bug 2026-03-12 18:42:23 +08:00
121 changed files with 13597 additions and 210 deletions

7
.env
View File

@@ -6,9 +6,12 @@ APIKEY=sk-85880595fc714d63bfd0b025e917bd26#千问apikey
# 962516e4-60eb-4a26-a5a3-44e21adcf7bc #豆包 # 962516e4-60eb-4a26-a5a3-44e21adcf7bc #豆包
# 消息回调ngrok 调通用,由 run-ngrok.sh 自动写入) # 消息回调ngrok 调通用,由 run-ngrok.sh 自动写入)
CALLBACK_BASE_URL=https://dissonant-destinee-nonsensibly.ngrok-free.dev # CALLBACK_BASE_URL=https://dissonant-destinee-nonsensibly.ngrok-free.dev
CALLBACK_BASE_URL=http://demo.bimwe.com
# 固定隧道代理socks5h不填登录页代理时后端自动用此处传给 7006 # 固定隧道代理socks5h不填登录页代理时后端自动用此处传给 7006
TUNNEL_PROXY=218.78.109.253:16816 # TUNNEL_PROXY=218.78.109.253:16816
TUNNEL_PROXY=14.103.95.78:16816
TUNNEL_PROXY_USERNAME=fawbjjkk TUNNEL_PROXY_USERNAME=fawbjjkk
TUNNEL_PROXY_PASSWORD=5hz6avfb TUNNEL_PROXY_PASSWORD=5hz6avfb

View File

@@ -4,6 +4,10 @@ PORT=3000
# BACKEND_HOST=127.0.0.1 # BACKEND_HOST=127.0.0.1
WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006 WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006
CHECK_STATUS_BASE_URL=http://113.44.162.180:7006 CHECK_STATUS_BASE_URL=http://113.44.162.180:7006
# 群发图片:默认用 7006 /message/SendImageMessageMsgItem.MsgType=0可覆盖
# SEND_IMAGE_UPSTREAM_BASE_URL=http://113.44.162.180:7006
# SEND_IMAGE_PATH=/message/SendImageMessage
# IMAGE_MSG_TYPE=0
# 第三方滑块(7765)iframe 加载自带预填表单页,提交到下方地址 # 第三方滑块(7765)iframe 加载自带预填表单页,提交到下方地址
SLIDER_VERIFY_BASE_URL=http://113.44.162.180:7765 SLIDER_VERIFY_BASE_URL=http://113.44.162.180:7765
SLIDER_VERIFY_KEY=408449830 SLIDER_VERIFY_KEY=408449830
@@ -16,7 +20,7 @@ SLIDER_VERIFY_KEY=408449830
# 固定隧道代理推荐socks5h + 用户名密码,与 requests 示例一致 # 固定隧道代理推荐socks5h + 用户名密码,与 requests 示例一致
# 不填登录页代理时,后端自动使用此处配置 # 不填登录页代理时,后端自动使用此处配置
TUNNEL_PROXY=218.78.109.253:16816 TUNNEL_PROXY=14.103.95.78:16816
TUNNEL_PROXY_USERNAME=fawbjjkk TUNNEL_PROXY_USERNAME=fawbjjkk
TUNNEL_PROXY_PASSWORD=5hz6avfb TUNNEL_PROXY_PASSWORD=5hz6avfb
@@ -32,6 +36,7 @@ TUNNEL_PROXY_PASSWORD=5hz6avfb
# 消息实时回调(主入口):设置后向 7006 注册 SetCallback新消息由 7006 POST 到本服务,不再走 WS # 消息实时回调(主入口):设置后向 7006 注册 SetCallback新消息由 7006 POST 到本服务,不再走 WS
# 需为 7006 能访问到的公网地址,例如 https://your-domain.com # 需为 7006 能访问到的公网地址,例如 https://your-domain.com
# 项目启动时会自动为所有已知账号 key 重新注册回调,重启后无需再手动设置
# CALLBACK_BASE_URL=https://your-domain.com # CALLBACK_BASE_URL=https://your-domain.com
# 千问 API Key用于个性化问候等优先 QWEN_API_KEY其次 APIKEY # 千问 API Key用于个性化问候等优先 QWEN_API_KEY其次 APIKEY

22
.env.prod.example Normal file
View File

@@ -0,0 +1,22 @@
# 生产环境变量模板(随项目提交)
# 部署时复制为 .env.prod 并填写真实值cp .env.prod.example .env.prod && 编辑 .env.prod
# run-docker.sh 仅读取 .env.prod不会读取 .env本地开发用
PORT=3003
BACKEND_PORT=8008
WECHAT_UPSTREAM_BASE_URL=http://113.44.162.180:7006
WS_KEY=HBpEnbtj9BJZ
SLIDER_VERIFY_KEY=408449830
APIKEY=sk-85880595fc714d63bfd0b025e917bd26#千问apikey
# 962516e4-60eb-4a26-a5a3-44e21adcf7bc #豆包
# 消息回调ngrok 调通用,由 run-ngrok.sh 自动写入)
CALLBACK_BASE_URL=http://demo.bimwe.com/
# 固定隧道代理socks5h不填登录页代理时后端自动用此处传给 7006
# TUNNEL_PROXY=218.78.109.253:16816
TUNNEL_PROXY=14.103.95.78:16816
TUNNEL_PROXY_USERNAME=fawbjjkk
TUNNEL_PROXY_PASSWORD=5hz6avfb

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# 本地环境(含密钥,不提交)
.env
.env.local
.env.*.local
# 生产环境(服务器上单独维护,不提交)
.env.prod
# 保留模板(随项目提交)
# .env.example
# .env.prod.example
# 依赖与构建
node_modules/
.venv/
venv/
__pycache__/
*.py[cod]
dist/
build/
*.egg-info/
.eggs/
# 数据与日志(可挂载卷)
backend/data/
data/
*.db
*.sqlite
*.log
# 系统与编辑器
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp
*.swo
*~
# 调试与临时
*.tmp
.cache/
.pytest_cache/
.coverage
htmlcov/

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,101 @@
Metadata-Version: 2.1
Name: httpx-socks
Version: 0.9.2
Summary: Proxy (HTTP, SOCKS) transports for httpx
Home-page: https://github.com/romis2012/httpx-socks
Author: Roman Snegirev
Author-email: snegiryev@gmail.com
License: Apache 2
Keywords: httpx asyncio socks socks5 socks4 http proxy
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Requires-Dist: httpx<0.28.0,>=0.21.0
Requires-Dist: httpcore<2.0,>=0.17.3
Requires-Dist: python-socks>=2.0.0
Provides-Extra: asyncio
Requires-Dist: async-timeout>=3.0.1; extra == "asyncio"
Provides-Extra: trio
Requires-Dist: trio>=0.16.0; extra == "trio"
# httpx-socks
[![CI](https://github.com/romis2012/httpx-socks/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/httpx-socks/actions/workflows/ci.yml)
[![Coverage Status](https://codecov.io/gh/romis2012/httpx-socks/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/httpx-socks)
[![PyPI version](https://badge.fury.io/py/httpx-socks.svg)](https://pypi.python.org/pypi/httpx-socks)
<!--
[![Downloads](https://pepy.tech/badge/httpx-socks/month)](https://pepy.tech/project/httpx-socks)
-->
The `httpx-socks` package provides proxy transports for [httpx](https://github.com/encode/httpx) client.
SOCKS4(a), SOCKS5(h), HTTP (tunneling) proxy supported.
It uses [python-socks](https://github.com/romis2012/python-socks) for core proxy functionality.
## Requirements
- Python >= 3.6
- httpx>=0.21.0
- python-socks>=2.0.0
- async-timeout>=3.0.1 (optional)
- trio>=0.16.0 (optional)
## Installation
only sync proxy support:
```
pip install httpx-socks
```
to include optional asyncio support (it requires async-timeout):
```
pip install httpx-socks[asyncio]
```
to include optional trio support:
```
pip install httpx-socks[trio]
```
## Usage
#### sync transport
```python
import httpx
from httpx_socks import SyncProxyTransport
def fetch(url):
transport = SyncProxyTransport.from_url('socks5://user:password@127.0.0.1:1080')
with httpx.Client(transport=transport) as client:
res = client.get(url)
return res.text
```
#### async transport (asyncio, trio)
```python
import httpx
from httpx_socks import AsyncProxyTransport
async def fetch(url):
transport = AsyncProxyTransport.from_url('socks5://user:password@127.0.0.1:1080')
async with httpx.AsyncClient(transport=transport) as client:
res = await client.get(url)
return res.text
```
#### secure proxy connections (aka "HTTPS proxies", experimental feature, both sync and async support)
```python
import ssl
import httpx
from httpx_socks import AsyncProxyTransport
async def fetch(url):
proxy_ssl = ssl.SSLContext(ssl.PROTOCOL_TLS)
proxy_ssl.verify_mode = ssl.CERT_REQUIRED
proxy_ssl.load_verify_locations(...)
transport = AsyncProxyTransport.from_url('http://user:password@127.0.0.1:8080', proxy_ssl=proxy_ssl)
async with httpx.AsyncClient(transport=transport) as client:
res = await client.get(url)
return res.text
```

View File

@@ -0,0 +1,19 @@
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/_async_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/_async_transport.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/_sync_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/_sync_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/httpx_socks/_sync_transport.cpython-39.pyc,,
httpx_socks-0.9.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
httpx_socks-0.9.2.dist-info/LICENSE.txt,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
httpx_socks-0.9.2.dist-info/METADATA,sha256=nYzxhiQ-G1XYBKP58owrMZWfJhGYCm6aKh7G4rn26zM,3007
httpx_socks-0.9.2.dist-info/RECORD,,
httpx_socks-0.9.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
httpx_socks-0.9.2.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
httpx_socks-0.9.2.dist-info/top_level.txt,sha256=w-NjPGd_PneJsrpI5kELbCCdbD3YlL8mgwP3WkawJ0s,12
httpx_socks/__init__.py,sha256=_iMI2My4TRAD66ZQgHrM3-CVQ8M-gGzLGVBibPF6hl0,448
httpx_socks/_async_proxy.py,sha256=RqnPas10Ux-VRACZu2qZsXsCxF-a5ZoeSw1VLmaSPYc,8452
httpx_socks/_async_transport.py,sha256=QEcg3Pb0hvMvcebKbolMdhrfdVuO1m-V_9-PrFOPF9A,3150
httpx_socks/_sync_proxy.py,sha256=vMVrR2-qKxmRHhn8lKFXXz-dDolAZIi96PeBp4YQF3g,6544
httpx_socks/_sync_stream.py,sha256=aaAH6bSPkAdHeDf2rvNDx_-lbu4kXFg1u2OMBNzQ3QQ,1421
httpx_socks/_sync_transport.py,sha256=FiqT2gekBI3V1Jq_uoFRa7biksLDljcEPrNFPq0LX-c,3054

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (75.3.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1 @@
httpx_socks

View File

@@ -0,0 +1,23 @@
__title__ = 'httpx-socks'
__version__ = '0.9.2'
from python_socks import (
ProxyError,
ProxyTimeoutError,
ProxyConnectionError,
ProxyType
)
from ._sync_transport import SyncProxyTransport
from ._async_transport import AsyncProxyTransport
__all__ = (
'__title__',
'__version__',
'SyncProxyTransport',
'AsyncProxyTransport',
'ProxyError',
'ProxyTimeoutError',
'ProxyConnectionError',
'ProxyType',
)

View File

@@ -0,0 +1,258 @@
import ssl
import sniffio
from httpcore import (
AsyncConnectionPool,
Origin,
AsyncConnectionInterface,
Request,
Response,
default_ssl_context,
AsyncHTTP11Connection,
ConnectionNotAvailable,
)
from httpcore import AsyncNetworkStream
from httpcore._synchronization import AsyncLock
from python_socks import ProxyType, parse_proxy_url
class AsyncProxy(AsyncConnectionPool):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
loop=None,
**kwargs,
):
self._proxy_type = proxy_type
self._proxy_host = proxy_host
self._proxy_port = proxy_port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._loop = loop
super().__init__(**kwargs)
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
return AsyncProxyConnection(
proxy_type=self._proxy_type,
proxy_host=self._proxy_host,
proxy_port=self._proxy_port,
username=self._username,
password=self._password,
rdns=self._rdns,
proxy_ssl=self._proxy_ssl,
loop=self._loop,
remote_origin=origin,
ssl_context=self._ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
)
@classmethod
def from_url(cls, url, **kwargs):
proxy_type, host, port, username, password = parse_proxy_url(url)
return cls(
proxy_type=proxy_type,
proxy_host=host,
proxy_port=port,
username=username,
password=password,
**kwargs,
)
class AsyncProxyConnection(AsyncConnectionInterface):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
loop=None,
remote_origin: Origin,
ssl_context: ssl.SSLContext,
keepalive_expiry: float = None,
http1: bool = True,
http2: bool = False,
) -> None:
if ssl_context is None: # pragma: no cover
ssl_context = default_ssl_context()
self._proxy_type = proxy_type
self._proxy_host = proxy_host
self._proxy_port = proxy_port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._loop = loop
self._remote_origin = remote_origin
self._ssl_context = ssl_context
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
self._http2 = http2
self._connect_lock = AsyncLock()
self._connection = None
self._connect_failed: bool = False
async def handle_async_request(self, request: Request) -> Response:
timeouts = request.extensions.get('timeout', {})
timeout = timeouts.get('connect', None)
try:
async with self._connect_lock:
if self._connection is None:
stream = await self._connect_via_proxy(
origin=self._remote_origin,
connect_timeout=timeout,
)
ssl_object = stream.get_extra_info("ssl_object")
http2_negotiated = (
ssl_object is not None and ssl_object.selected_alpn_protocol() == "h2"
)
if http2_negotiated or (self._http2 and not self._http1):
from httpcore import AsyncHTTP2Connection
self._connection = AsyncHTTP2Connection(
origin=self._remote_origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
else:
self._connection = AsyncHTTP11Connection(
origin=self._remote_origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
elif not self._connection.is_available(): # pragma: no cover
raise ConnectionNotAvailable()
except BaseException as exc:
self._connect_failed = True
raise exc
return await self._connection.handle_async_request(request)
async def _connect_via_proxy(self, origin, connect_timeout) -> AsyncNetworkStream:
scheme, hostname, port = origin.scheme, origin.host, origin.port
ssl_context = self._ssl_context if scheme == b'https' else None
host = hostname.decode('ascii') # ?
return await self._open_stream(
host=host,
port=port,
connect_timeout=connect_timeout,
ssl_context=ssl_context,
)
async def _open_stream(self, host, port, connect_timeout, ssl_context):
backend = sniffio.current_async_library()
if backend == 'asyncio':
return await self._open_aio_stream(host, port, connect_timeout, ssl_context)
if backend == 'trio':
return await self._open_trio_stream(host, port, connect_timeout, ssl_context)
# Curio support has been dropped in httpcore 0.14.0
# if backend == 'curio':
# return await self._open_curio_stream(host, port, connect_timeout, ssl_context)
raise RuntimeError(f'Unsupported concurrency backend {backend!r}') # pragma: no cover
async def _open_aio_stream(self, host, port, connect_timeout, ssl_context):
from httpcore._backends.anyio import AnyIOStream
from python_socks.async_.anyio import Proxy
proxy = Proxy.create(
proxy_type=self._proxy_type,
host=self._proxy_host,
port=self._proxy_port,
username=self._username,
password=self._password,
rdns=self._rdns,
proxy_ssl=self._proxy_ssl,
)
proxy_stream = await proxy.connect(
host,
port,
dest_ssl=ssl_context,
timeout=connect_timeout,
)
return AnyIOStream(proxy_stream.anyio_stream)
async def _open_trio_stream(self, host, port, connect_timeout, ssl_context):
from httpcore._backends.trio import TrioStream
from python_socks.async_.trio.v2 import Proxy
proxy = Proxy.create(
proxy_type=self._proxy_type,
host=self._proxy_host,
port=self._proxy_port,
username=self._username,
password=self._password,
rdns=self._rdns,
proxy_ssl=self._proxy_ssl,
)
proxy_stream = await proxy.connect(
host,
port,
dest_ssl=ssl_context,
timeout=connect_timeout,
)
return TrioStream(proxy_stream.trio_stream)
async def aclose(self) -> None:
if self._connection is not None:
await self._connection.aclose()
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._remote_origin
def is_available(self) -> bool:
if self._connection is None: # pragma: no cover
# return self._http2 and (self._remote_origin.scheme == b"https" or not self._http1)
return False
return self._connection.is_available()
def has_expired(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.has_expired()
def is_idle(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.is_idle()
def is_closed(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.is_closed()
def info(self) -> str: # pragma: no cover
if self._connection is None:
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
return self._connection.info()

View File

@@ -0,0 +1,104 @@
import ssl
import typing
import httpcore
from httpx import AsyncBaseTransport, Request, Response, AsyncByteStream, Limits
# noinspection PyProtectedMember
from httpx._config import DEFAULT_LIMITS, create_ssl_context
# noinspection PyProtectedMember
from httpx._transports.default import AsyncResponseStream, map_httpcore_exceptions
from python_socks import ProxyType, parse_proxy_url
from ._async_proxy import AsyncProxy
class AsyncProxyTransport(AsyncBaseTransport):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
verify=True,
cert=None,
trust_env: bool = True,
limits: Limits = DEFAULT_LIMITS,
**kwargs,
):
ssl_context = create_ssl_context(
verify=verify,
cert=cert,
trust_env=trust_env,
http2=kwargs.get('http2', False),
)
self._pool = AsyncProxy(
proxy_type=proxy_type,
proxy_host=proxy_host,
proxy_port=proxy_port,
username=username,
password=password,
rdns=rdns,
proxy_ssl=proxy_ssl,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
**kwargs,
)
async def handle_async_request(self, request: Request) -> Response:
assert isinstance(request.stream, AsyncByteStream)
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
resp = await self._pool.handle_async_request(req)
assert isinstance(resp.stream, typing.AsyncIterable)
return Response(
status_code=resp.status,
headers=resp.headers,
stream=AsyncResponseStream(resp.stream),
extensions=resp.extensions,
)
@classmethod
def from_url(cls, url, **kwargs):
proxy_type, host, port, username, password = parse_proxy_url(url)
return cls(
proxy_type=proxy_type,
proxy_host=host,
proxy_port=port,
username=username,
password=password,
**kwargs,
)
async def aclose(self) -> None:
await self._pool.aclose() # pragma: no cover
async def __aenter__(self):
await self._pool.__aenter__()
return self
async def __aexit__(self, exc_type=None, exc_value=None, traceback=None):
with map_httpcore_exceptions():
await self._pool.__aexit__(exc_type, exc_value, traceback)

View File

@@ -0,0 +1,206 @@
import ssl
from httpcore import (
ConnectionPool,
Origin,
ConnectionInterface,
Request,
Response,
default_ssl_context,
HTTP11Connection,
ConnectionNotAvailable,
)
# from httpcore.backends.sync import SyncStream
from ._sync_stream import SyncStream
from httpcore._synchronization import Lock
from python_socks import ProxyType, parse_proxy_url
from python_socks.sync.v2 import Proxy
class SyncProxy(ConnectionPool):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
**kwargs,
):
self._proxy_type = proxy_type
self._proxy_host = proxy_host
self._proxy_port = proxy_port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
super().__init__(**kwargs)
def create_connection(self, origin: Origin) -> ConnectionInterface:
return SyncProxyConnection(
proxy_type=self._proxy_type,
proxy_host=self._proxy_host,
proxy_port=self._proxy_port,
username=self._username,
password=self._password,
rdns=self._rdns,
proxy_ssl=self._proxy_ssl,
remote_origin=origin,
ssl_context=self._ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
)
@classmethod
def from_url(cls, url, **kwargs):
proxy_type, host, port, username, password = parse_proxy_url(url)
return cls(
proxy_type=proxy_type,
proxy_host=host,
proxy_port=port,
username=username,
password=password,
**kwargs,
)
class SyncProxyConnection(ConnectionInterface):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
remote_origin: Origin,
ssl_context: ssl.SSLContext,
keepalive_expiry: float = None,
http1: bool = True,
http2: bool = False,
) -> None:
if ssl_context is None: # pragma: no cover
ssl_context = default_ssl_context()
self._proxy_type = proxy_type
self._proxy_host = proxy_host
self._proxy_port = proxy_port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._remote_origin = remote_origin
self._ssl_context = ssl_context
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
self._http2 = http2
self._connect_lock = Lock()
self._connection = None
self._connect_failed: bool = False
def handle_request(self, request: Request) -> Response:
timeouts = request.extensions.get('timeout', {})
timeout = timeouts.get('connect', None)
try:
with self._connect_lock:
if self._connection is None:
stream = self._connect_via_proxy(
origin=self._remote_origin,
connect_timeout=timeout,
)
ssl_object = stream.get_extra_info('ssl_object')
http2_negotiated = (
ssl_object is not None and ssl_object.selected_alpn_protocol() == "h2"
)
if http2_negotiated or (self._http2 and not self._http1):
from httpcore import HTTP2Connection
self._connection = HTTP2Connection(
origin=self._remote_origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
else:
self._connection = HTTP11Connection(
origin=self._remote_origin,
stream=stream,
keepalive_expiry=self._keepalive_expiry,
)
elif not self._connection.is_available(): # pragma: no cover
raise ConnectionNotAvailable()
except BaseException as exc:
self._connect_failed = True
raise exc
return self._connection.handle_request(request)
def _connect_via_proxy(self, origin: Origin, connect_timeout: int):
scheme, hostname, port = origin.scheme, origin.host, origin.port
ssl_context = self._ssl_context if scheme == b'https' else None
host = hostname.decode('ascii')
proxy = Proxy.create(
proxy_type=self._proxy_type,
host=self._proxy_host,
port=self._proxy_port,
username=self._username,
password=self._password,
rdns=self._rdns,
proxy_ssl=self._proxy_ssl,
)
proxy_stream = proxy.connect(
host,
port,
dest_ssl=ssl_context,
timeout=connect_timeout,
)
return SyncStream(sock=proxy_stream.socket)
def close(self) -> None:
if self._connection is not None:
self._connection.close()
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._remote_origin
def is_available(self) -> bool:
if self._connection is None: # pragma: no cover
# return self._http2 and (self._remote_origin.scheme == b"https" or not self._http1)
return False
return self._connection.is_available()
def has_expired(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.has_expired()
def is_idle(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.is_idle()
def is_closed(self) -> bool:
if self._connection is None:
return self._connect_failed
return self._connection.is_closed()
def info(self) -> str: # pragma: no cover
if self._connection is None:
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
return self._connection.info()

View File

@@ -0,0 +1,40 @@
import ssl
import typing
from httpcore._backends.sync import SyncStream as CoreSyncStream
from httpcore._utils import is_socket_readable
from python_socks.sync.v2._ssl_transport import SSLTransport
class SyncStream(CoreSyncStream):
def get_extra_info(self, info: str) -> typing.Any:
if info == "ssl_object":
if isinstance(self._sock, ssl.SSLSocket):
# noinspection PyProtectedMember
return self._sock._sslobj # type: ignore
if isinstance(self._sock, SSLTransport):
return self._sock.sslobj # type: ignore
return None
if info == "client_addr": # pragma: nocover
if isinstance(self._sock, SSLTransport):
return self._sock.socket.getsockname()
else:
return self._sock.getsockname()
if info == "server_addr": # pragma: nocover
if isinstance(self._sock, SSLTransport):
return self._sock.socket.getpeername()
else:
return self._sock.getpeername()
if info == "socket": # pragma: nocover
return self._sock # ???
if info == "is_readable":
if isinstance(self._sock, SSLTransport):
return is_socket_readable(self._sock.socket)
else:
return is_socket_readable(self._sock)
return None # pragma: nocover

View File

@@ -0,0 +1,105 @@
import ssl
import typing
import httpcore
from httpx import BaseTransport, Request, Response, SyncByteStream, Limits
# noinspection PyProtectedMember
from httpx._config import DEFAULT_LIMITS, create_ssl_context
# noinspection PyProtectedMember
from httpx._transports.default import ResponseStream, map_httpcore_exceptions
from ._sync_proxy import SyncProxy
from python_socks import ProxyType, parse_proxy_url
class SyncProxyTransport(BaseTransport):
def __init__(
self,
*,
proxy_type: ProxyType,
proxy_host: str,
proxy_port: int,
username=None,
password=None,
rdns=None,
proxy_ssl: ssl.SSLContext = None,
verify=True,
cert=None,
trust_env: bool = True,
limits: Limits = DEFAULT_LIMITS,
**kwargs,
):
ssl_context = create_ssl_context(
verify=verify,
cert=cert,
trust_env=trust_env,
http2=kwargs.get('http2', False),
)
self._pool = SyncProxy(
proxy_type=proxy_type,
proxy_host=proxy_host,
proxy_port=proxy_port,
username=username,
password=password,
rdns=rdns,
proxy_ssl=proxy_ssl,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
**kwargs,
)
def handle_request(self, request: Request) -> Response:
assert isinstance(request.stream, SyncByteStream)
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
resp = self._pool.handle_request(req)
assert isinstance(resp.stream, typing.Iterable)
return Response(
status_code=resp.status,
headers=resp.headers,
stream=ResponseStream(resp.stream),
extensions=resp.extensions,
)
@classmethod
def from_url(cls, url, **kwargs):
proxy_type, host, port, username, password = parse_proxy_url(url)
return cls(
proxy_type=proxy_type,
proxy_host=host,
proxy_port=port,
username=username,
password=password,
**kwargs,
)
def close(self) -> None:
self._pool.close() # pragma: no cover
def __enter__(self):
self._pool.__enter__()
return self
def __exit__(self, exc_type=None, exc_value=None, traceback=None):
with map_httpcore_exceptions():
self._pool.__exit__(exc_type, exc_value, traceback)

View File

@@ -0,0 +1,295 @@
Metadata-Version: 2.4
Name: python-socks
Version: 2.8.1
Summary: Proxy (SOCKS4, SOCKS5, HTTP CONNECT) client for Python
Author-email: Roman Snegirev <snegiryev@gmail.com>
License: Apache-2.0
Project-URL: homepage, https://github.com/romis2012/python-socks
Project-URL: repository, https://github.com/romis2012/python-socks
Keywords: socks,socks5,socks4,http,proxy,asyncio,trio,curio,anyio
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Intended Audience :: Developers
Classifier: Framework :: AsyncIO
Classifier: Framework :: Trio
Classifier: License :: OSI Approved :: Apache Software License
Requires-Python: >=3.8.0
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Provides-Extra: asyncio
Requires-Dist: async-timeout>=4.0; python_version < "3.11" and extra == "asyncio"
Provides-Extra: trio
Requires-Dist: trio>=0.24; extra == "trio"
Provides-Extra: curio
Requires-Dist: curio>=1.4; extra == "curio"
Provides-Extra: anyio
Requires-Dist: anyio<5.0.0,>=3.3.4; extra == "anyio"
Dynamic: license-file
## python-socks
[![CI](https://github.com/romis2012/python-socks/actions/workflows/ci.yml/badge.svg)](https://github.com/romis2012/python-socks/actions/workflows/ci.yml)
[![Coverage Status](https://codecov.io/gh/romis2012/python-socks/branch/master/graph/badge.svg)](https://codecov.io/gh/romis2012/python-socks)
[![PyPI version](https://badge.fury.io/py/python-socks.svg)](https://pypi.python.org/pypi/python-socks)
[![versions](https://img.shields.io/pypi/pyversions/python-socks.svg)](https://github.com/romis2012/python-socks)
<!--
[![Downloads](https://pepy.tech/badge/python-socks/month)](https://pepy.tech/project/python-socks)
-->
The `python-socks` package provides a core proxy client functionality for Python.
Supports `SOCKS4(a)`, `SOCKS5(h)`, `HTTP CONNECT` proxy and provides sync and async (asyncio, trio, curio, anyio) APIs.
You probably don't need to use `python-socks` directly.
It is used internally by
[aiohttp-socks](https://github.com/romis2012/aiohttp-socks) and [httpx-socks](https://github.com/romis2012/httpx-socks) packages.
## Requirements
- Python >= 3.8
- async-timeout >= 4.0 (optional)
- trio >= 0.24 (optional)
- curio >= 1.4 (optional)
- anyio >= 3.3.4 (optional)
## Installation
only sync proxy support:
```
pip install python-socks
```
to include optional asyncio support:
```
pip install python-socks[asyncio]
```
to include optional trio support:
```
pip install python-socks[trio]
```
to include optional curio support:
```
pip install python-socks[curio]
```
to include optional anyio support:
```
pip install python-socks[anyio]
```
## Simple usage
We are making secure HTTP GET request via SOCKS5 proxy
#### Sync
```python
import ssl
from python_socks.sync import Proxy
proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080')
# `connect` returns standard Python socket in blocking mode
sock = proxy.connect(dest_host='check-host.net', dest_port=443)
sock = ssl.create_default_context().wrap_socket(
sock=sock,
server_hostname='check-host.net'
)
request = (
b'GET /ip HTTP/1.1\r\n'
b'Host: check-host.net\r\n'
b'Connection: close\r\n\r\n'
)
sock.sendall(request)
response = sock.recv(4096)
print(response)
```
#### Async (asyncio)
```python
import ssl
import asyncio
from python_socks.async_.asyncio import Proxy
proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080')
# `connect` returns standard Python socket in non-blocking mode
# so we can pass it to asyncio.open_connection(...)
sock = await proxy.connect(dest_host='check-host.net', dest_port=443)
reader, writer = await asyncio.open_connection(
host=None,
port=None,
sock=sock,
ssl=ssl.create_default_context(),
server_hostname='check-host.net',
)
request = (
b'GET /ip HTTP/1.1\r\n'
b'Host: check-host.net\r\n'
b'Connection: close\r\n\r\n'
)
writer.write(request)
response = await reader.read(-1)
print(response)
```
#### Async (trio)
```python
import ssl
import trio
from python_socks.async_.trio import Proxy
proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080')
# `connect` returns trio socket
# so we can pass it to trio.SocketStream
sock = await proxy.connect(dest_host='check-host.net', dest_port=443)
stream = trio.SocketStream(sock)
stream = trio.SSLStream(
stream, ssl.create_default_context(),
server_hostname='check-host.net'
)
await stream.do_handshake()
request = (
b'GET /ip HTTP/1.1\r\n'
b'Host: check-host.net\r\n'
b'Connection: close\r\n\r\n'
)
await stream.send_all(request)
response = await stream.receive_some(4096)
print(response)
```
#### Async (curio)
```python
import curio.ssl as curiossl
from python_socks.async_.curio import Proxy
proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080')
# `connect` returns curio.io.Socket
sock = await proxy.connect(
dest_host='check-host.net',
dest_port=443
)
request = (
b'GET /ip HTTP/1.1\r\n'
b'Host: check-host.net\r\n'
b'Connection: close\r\n\r\n'
)
ssl_context = curiossl.create_default_context()
sock = await ssl_context.wrap_socket(
sock, do_handshake_on_connect=False, server_hostname='check-host.net'
)
await sock.do_handshake()
stream = sock.as_stream()
await stream.write(request)
response = await stream.read(1024)
print(response)
```
#### Async (anyio)
```python
import ssl
from python_socks.async_.anyio import Proxy
proxy = Proxy.from_url('socks5://user:password@127.0.0.1:1080')
# `connect` returns AnyioSocketStream
stream = await proxy.connect(
dest_host='check-host.net',
dest_port=443,
dest_ssl=ssl.create_default_context(),
)
request = (
b'GET /ip HTTP/1.1\r\n'
b'Host: check-host.net\r\n'
b'Connection: close\r\n\r\n'
)
await stream.write_all(request)
response = await stream.read()
print(response)
```
## More complex example
#### A urllib3 PoolManager that routes connections via the proxy
```python
from urllib3 import PoolManager, HTTPConnectionPool, HTTPSConnectionPool
from urllib3.connection import HTTPConnection, HTTPSConnection
from python_socks.sync import Proxy
class ProxyHTTPConnection(HTTPConnection):
def __init__(self, *args, **kwargs):
socks_options = kwargs.pop('_socks_options')
self._proxy_url = socks_options['proxy_url']
super().__init__(*args, **kwargs)
def _new_conn(self):
proxy = Proxy.from_url(self._proxy_url)
return proxy.connect(
dest_host=self.host,
dest_port=self.port,
timeout=self.timeout
)
class ProxyHTTPSConnection(ProxyHTTPConnection, HTTPSConnection):
pass
class ProxyHTTPConnectionPool(HTTPConnectionPool):
ConnectionCls = ProxyHTTPConnection
class ProxyHTTPSConnectionPool(HTTPSConnectionPool):
ConnectionCls = ProxyHTTPSConnection
class ProxyPoolManager(PoolManager):
def __init__(self, proxy_url, timeout=5, num_pools=10, headers=None,
**connection_pool_kw):
connection_pool_kw['_socks_options'] = {'proxy_url': proxy_url}
connection_pool_kw['timeout'] = timeout
super().__init__(num_pools, headers, **connection_pool_kw)
self.pool_classes_by_scheme = {
'http': ProxyHTTPConnectionPool,
'https': ProxyHTTPSConnectionPool,
}
### and how to use it
manager = ProxyPoolManager('socks5://user:password@127.0.0.1:1080')
response = manager.request('GET', 'https://check-host.net/ip')
print(response.data)
```

View File

@@ -0,0 +1,149 @@
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_abc.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/abc.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/factory_async.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/factory_sync.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/http_async.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/http_sync.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/socks4_async.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/socks4_sync.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/socks5_async.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_connectors/socks5_sync.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_errors.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_helpers.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_protocols/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_protocols/errors.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_protocols/http.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_protocols/socks4.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_protocols/socks5.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_types.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/_version.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/_proxy_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/_resolver.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/v2/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/v2/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/v2/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/v2/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/anyio/v2/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/_resolver.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/v2/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/v2/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/v2/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/v2/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/asyncio/v2/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/curio/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/curio/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/curio/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/curio/_resolver.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/curio/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/_resolver.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/v2/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/v2/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/v2/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/v2/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/async_/trio/v2/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/_resolver.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/_stream.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/__init__.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/_chain.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/_connect.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/_proxy.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/_ssl_transport.cpython-39.pyc,,
../../../../../../../../Library/Caches/com.apple.python/Users/dannier/Desktop/living/AICLW/wechatAiclaw/.venv/lib/python3.9/site-packages/python_socks/sync/v2/_stream.cpython-39.pyc,,
python_socks-2.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
python_socks-2.8.1.dist-info/METADATA,sha256=jDS83DnufjrnQhuWQBgKuXA7h0KMUNre3G83vwrOXC4,8151
python_socks-2.8.1.dist-info/RECORD,,
python_socks-2.8.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
python_socks-2.8.1.dist-info/licenses/LICENSE.txt,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
python_socks-2.8.1.dist-info/top_level.txt,sha256=7qyDAVwjTZ0sIYG1iatFbE-SLpWhONjaoMQ0Ib2DX0Q,13
python_socks/__init__.py,sha256=gKywvwx_x4_0hWJEa2eif1dPzP9YeFXJdrNlHuvFmC8,367
python_socks/_abc.py,sha256=_juTZE0iqoTChE53IHjBKEYFO-fxYZfPhdUc6EgFlwE,909
python_socks/_connectors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
python_socks/_connectors/abc.py,sha256=1fgjPiv64UAMiJDsJXYr-GofPGhuHTwTzaodQfrzf7Q,397
python_socks/_connectors/factory_async.py,sha256=X63hJuhpr8iDdAxwrUlsbyXfHewZJtgKQRdSZs41RS4,1055
python_socks/_connectors/factory_sync.py,sha256=QVaNbUXSEPZW9Cv43gRFF3kwVwU64moQR1BEtE4VOiE,1042
python_socks/_connectors/http_async.py,sha256=vo8QhluTYEqnIoxL2XPcj9vSl8yhu9Tvm4KI-gMyJGM,951
python_socks/_connectors/http_sync.py,sha256=Q6DRrUCljokQe2veoJJ6G8SZ7QEmq8x4_Q-faSADLEY,926
python_socks/_connectors/socks4_async.py,sha256=QGoB1are0qU-uoPCUvanFBUvSCyFxgKN_XVMKSrDoYY,1179
python_socks/_connectors/socks4_sync.py,sha256=yX4e3xMOHH1OzVPTYROeQDBf8gcZcXODFOxBHyRtVOc,1148
python_socks/_connectors/socks5_async.py,sha256=qniozb0P8gtXF_vK3b7kjRmoGNMHmYAb9eIZg-heI8k,2829
python_socks/_connectors/socks5_sync.py,sha256=G_FW1XsiaMDbzR7vcBMtRpD3R9LpO5V5zu9pltpo7xI,2602
python_socks/_errors.py,sha256=M2KkgCrFy_ebEqw7JoxCWMjIIQ5Zu_B7YlXYhsLo-gk,329
python_socks/_helpers.py,sha256=UacO07M9i8G8CfqzBYWnehgCTFbZTlwJWAsFoXR3mVk,2708
python_socks/_protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
python_socks/_protocols/errors.py,sha256=4XqU4AqgrWaedmUVGMjd6TvQKXv3mV0H76s5lf8aazY,150
python_socks/_protocols/http.py,sha256=QhLHqBgc6DfsvC8AIhylN1tjfk7QzxgwemrK1LuL3vk,4499
python_socks/_protocols/socks4.py,sha256=j1Psqi3PDiKAF19g_Mh3sf_RwXu7o-8X7BqWOaoTbKE,3061
python_socks/_protocols/socks5.py,sha256=1PEr1s4JxmpST9OYEeVgbidnV_d4Vb85JZHmWqK-r9c,9713
python_socks/_types.py,sha256=dgQifNf2mIwdKoX7YmI91H_lUsW9-Y0ZkZGlIV6_NNw,90
python_socks/_version.py,sha256=e7j6da3GYTfq-hRQ9-hzCSija3gACzgMjjpG6A56uGs,49
python_socks/async_/__init__.py,sha256=TeBn5oirLy6aPoWcTc3OW9OxXh5Uv5LADwF_Y7f7HjY,64
python_socks/async_/_proxy_chain.py,sha256=TiG1Uu45anONWi2ZFj11w4-LrNbja8SbG27bWCX9Et4,987
python_socks/async_/anyio/__init__.py,sha256=pZnrND4Qd-pIGg7KI1ML41kZNkWHmkd3vrvwZZIJyk4,106
python_socks/async_/anyio/_chain.py,sha256=-d4sR-JM8Agh73rB8nK7DBcB9oqDgtx2GOTAx-SMslU,1099
python_socks/async_/anyio/_connect.py,sha256=dYx-X2rDDTe-mtLm6DTpV-gFB7Vt_OYS1Xs_4Ea9JZ4,306
python_socks/async_/anyio/_proxy.py,sha256=nWHzIFPYPXatmuVITdDWN6uoQe_P2Xm6NomgfgYm5UA,4322
python_socks/async_/anyio/_resolver.py,sha256=LPRLmEzgKPGCqAbEMmLSMVtFkNVKha_2u08MhrZZ-CY,590
python_socks/async_/anyio/_stream.py,sha256=OcCl4YkccA0-5hZh90CP4YmsBDLPwHpS3B0wCLytnAY,1614
python_socks/async_/anyio/v2/__init__.py,sha256=4xgyAJHDGpRANYVNfpUdn3Zo8Ib_gREWH8bH4APycK4,117
python_socks/async_/anyio/v2/_chain.py,sha256=NZ_9dA1AQrG3pBOnYBszbtD-Kse85bQ_dT33VtmGx3o,797
python_socks/async_/anyio/v2/_connect.py,sha256=_LF1f_cYJbfdSqkq-xEK4nF4l-bP5I1VWRFFyX1RH3Y,368
python_socks/async_/anyio/v2/_proxy.py,sha256=7F-Y_xhVcqGnKjUDVnjYNkyz5VEFJjzZhkl0Xklyj9U,4085
python_socks/async_/anyio/v2/_stream.py,sha256=40D3Pmb0rryk9gaQlmjpAMdiJi5H5O0xbBseblGG1FY,1616
python_socks/async_/asyncio/__init__.py,sha256=0WAM07vy0YvI8IhKGmaUA0GaNLHaG3l-4mnJVbfoqHE,65
python_socks/async_/asyncio/_connect.py,sha256=K4qqOwXI-1uVleSwf-mQ2ZPO5LwKeSr50Wr3jWImEng,1110
python_socks/async_/asyncio/_proxy.py,sha256=HS1NfXZCYOZ2b4qUee01CwH4NF1OcXPzqYxEJXYJeT8,4120
python_socks/async_/asyncio/_resolver.py,sha256=ROqOncuIub87J9_nMdvRTG_5zYOh8uafk265z0s2ORQ,681
python_socks/async_/asyncio/_stream.py,sha256=RP7BIP9iFFy5nLRf9kHH7s1h_zPfo0yoEVKNpExqFcI,1021
python_socks/async_/asyncio/v2/__init__.py,sha256=v9eJQeZ2yGJHeBQ0FYqNmIMCRrB547QqoAANHVBitxY,108
python_socks/async_/asyncio/v2/_chain.py,sha256=KCtmKg0halaCIEqBY6JB-KGVNNiaa4APBb5l5z8kVPk,811
python_socks/async_/asyncio/v2/_connect.py,sha256=3yPmPCNmMKlCCZZCnNvydER3ilnfE5gTevzBN7IoGEE,603
python_socks/async_/asyncio/v2/_proxy.py,sha256=QPMx2nFeHCKObBkDqzONkgf2VLXdhuP-aMsBWAgdf-I,4714
python_socks/async_/asyncio/v2/_stream.py,sha256=YdpJap_Ax-7tJnA-AJ7nayZmD5lpem2UIPbUepnzqxM,2753
python_socks/async_/curio/__init__.py,sha256=1OyhLhvLJ23iDROgyxGqQACice66SrIYCMqmwjw_dqY,63
python_socks/async_/curio/_connect.py,sha256=u1HRJ8n3OZbux-wETO9HuqZv9By2LGhhSsM78gnxr_0,328
python_socks/async_/curio/_proxy.py,sha256=_Qgodw62VPvt-5j1T34l7P52R5Bk4Crq97QJ0f1Qf10,3644
python_socks/async_/curio/_resolver.py,sha256=_jDZq_lzTl_QihxH2HNEY_7_ELo-qmzG6IO5PNNEOek,722
python_socks/async_/curio/_stream.py,sha256=sIAbwkPuiFF4c8cEs7exVdGb7cfFgoggKTV4LYqDcm0,855
python_socks/async_/trio/__init__.py,sha256=Y68xwGqkXGKXHcwM6nvev6n2ChbFpBUFW0RzBrsIrug,61
python_socks/async_/trio/_connect.py,sha256=qVfK4jz3h1Tjwnr8sX0DNazASdg3DDXNHjMopaHGY0I,851
python_socks/async_/trio/_proxy.py,sha256=yp3NJOTAaw2ReXlPt4fBRY51Xqc1C3tf16H3v0XvlCg,3786
python_socks/async_/trio/_resolver.py,sha256=uqBoTkbYpwwESoph2SmXRUUHM_DsT7dALQDmKqp-sco,591
python_socks/async_/trio/_stream.py,sha256=xunx_XUiwH1pBLQjWbkqq3P92SeXaRypPMPRYwHOsPg,1003
python_socks/async_/trio/v2/__init__.py,sha256=cd8Sc6NLmrUZewkqeDvnxmQqGZBKNuwYEEb4NMW2qo0,116
python_socks/async_/trio/v2/_chain.py,sha256=g9atl9HcWYrCHLCI8GtLR9GZC67sKiXEjcvGwZVGhDw,795
python_socks/async_/trio/v2/_connect.py,sha256=Nk0ipvuFFnzH-NPfXkX57I74-sbT6ks--ZgbfN3r42o,360
python_socks/async_/trio/v2/_proxy.py,sha256=9118wyv9SthZNVlvECfb3OfhrPMOFyyJSHhl7lG0TbI,4094
python_socks/async_/trio/v2/_stream.py,sha256=wGB7sDjRO1ywVNjF_DFgdF2orjD3zrh8dPSmW9aBEV0,1475
python_socks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
python_socks/sync/__init__.py,sha256=Dslh61MLGktjBK50UVGYjAQ-iGl8tjoLR93_-cvddWM,106
python_socks/sync/_chain.py,sha256=do_WtGenqKAG7B50nNZdltFqEQV8t-mSlGnMACNIwgE,973
python_socks/sync/_connect.py,sha256=N1Y-uQXkOiJ_dxkwWHw1fBliIQlTL47ydql7IvaSqm8,344
python_socks/sync/_proxy.py,sha256=Ny2QA8GI2aCUm0JL8IfY2j2IaXKzAZNp6bZeF7OmwYA,3287
python_socks/sync/_resolver.py,sha256=2rf9-NdjRktYrab1Oyv9ZJLKByP95ysFmRE3TgjRCSA,547
python_socks/sync/_stream.py,sha256=VeR-_ERvn5x_PjxFsPy88llEiNfTuejvXoZkAxlqOuY,811
python_socks/sync/v2/__init__.py,sha256=eVZc6zh0pLhNMf_uSPZiH_1vNGEvedwzX5gObUSkRT8,116
python_socks/sync/v2/_chain.py,sha256=IRVMioTVWwxylZ8maD5Gqxhhgw4VyrG5vo_hAOuEn8Q,579
python_socks/sync/v2/_connect.py,sha256=QrZ1CZimYCUMUuaD8JdUblwlqlzIbqFrbsPwMzoxGkM,420
python_socks/sync/v2/_proxy.py,sha256=7FuO6wML0DkEonoGAprhTlVd8fazOgNeNrJyxABmP1Q,3621
python_socks/sync/v2/_ssl_transport.py,sha256=MURwzvKu-S2-MI7o8KaZTvvBFOiiKIu5R0ERVIMvERE,6088
python_socks/sync/v2/_stream.py,sha256=Jq89x1xmIX3L_v-WyS1nRTItjTnYTcDcfDblsANxfJI,1544

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (82.0.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1 @@
python_socks

View File

@@ -0,0 +1,20 @@
from ._version import __version__, __title__
from ._types import ProxyType
from ._helpers import parse_proxy_url
from ._errors import (
ProxyError,
ProxyTimeoutError,
ProxyConnectionError,
)
__all__ = (
'__title__',
'__version__',
'ProxyError',
'ProxyTimeoutError',
'ProxyConnectionError',
'ProxyType',
'parse_proxy_url',
)

View File

@@ -0,0 +1,40 @@
from typing import Optional
class SyncResolver:
def resolve(self, host, port=0, family=0):
raise NotImplementedError()
class AsyncResolver:
async def resolve(self, host, port=0, family=0):
raise NotImplementedError()
class SyncSocketStream:
def write_all(self, data: bytes):
raise NotImplementedError()
def read(self, max_bytes: Optional[int] = None):
raise NotImplementedError()
def read_exact(self, n: int):
raise NotImplementedError()
def close(self):
raise NotImplementedError()
class AsyncSocketStream:
async def write_all(self, data: bytes):
raise NotImplementedError()
async def read(self, max_bytes: Optional[int] = None):
raise NotImplementedError()
async def read_exact(self, n: int):
raise NotImplementedError()
async def close(self):
raise NotImplementedError()

View File

@@ -0,0 +1,21 @@
from .._abc import SyncSocketStream, AsyncSocketStream
class SyncConnector:
def connect(
self,
stream: SyncSocketStream,
host: str,
port: int,
):
raise NotImplementedError
class AsyncConnector:
async def connect(
self,
stream: AsyncSocketStream,
host: str,
port: int,
):
raise NotImplementedError

View File

@@ -0,0 +1,40 @@
from typing import Optional
from .._abc import AsyncResolver
from .._types import ProxyType
from .abc import AsyncConnector
from .socks5_async import Socks5AsyncConnector
from .socks4_async import Socks4AsyncConnector
from .http_async import HttpAsyncConnector
def create_connector(
proxy_type: ProxyType,
username: Optional[str],
password: Optional[str],
rdns: Optional[bool],
resolver: AsyncResolver,
) -> AsyncConnector:
if proxy_type == ProxyType.SOCKS4:
return Socks4AsyncConnector(
user_id=username,
rdns=rdns,
resolver=resolver,
)
if proxy_type == ProxyType.SOCKS5:
return Socks5AsyncConnector(
username=username,
password=password,
rdns=rdns,
resolver=resolver,
)
if proxy_type == ProxyType.HTTP:
return HttpAsyncConnector(
username=username,
password=password,
resolver=resolver,
)
raise ValueError(f'Invalid proxy type: {proxy_type}')

View File

@@ -0,0 +1,40 @@
from typing import Optional
from .._abc import SyncResolver
from .._types import ProxyType
from .abc import SyncConnector
from .socks5_sync import Socks5SyncConnector
from .socks4_sync import Socks4SyncConnector
from .http_sync import HttpSyncConnector
def create_connector(
proxy_type: ProxyType,
username: Optional[str],
password: Optional[str],
rdns: Optional[bool],
resolver: SyncResolver,
) -> SyncConnector:
if proxy_type == ProxyType.SOCKS4:
return Socks4SyncConnector(
user_id=username,
rdns=rdns,
resolver=resolver,
)
if proxy_type == ProxyType.SOCKS5:
return Socks5SyncConnector(
username=username,
password=password,
rdns=rdns,
resolver=resolver,
)
if proxy_type == ProxyType.HTTP:
return HttpSyncConnector(
username=username,
password=password,
resolver=resolver,
)
raise ValueError(f'Invalid proxy type: {proxy_type}')

View File

@@ -0,0 +1,38 @@
from typing import Optional
from .._abc import AsyncSocketStream, AsyncResolver
from .abc import AsyncConnector
from .._protocols import http
class HttpAsyncConnector(AsyncConnector):
def __init__(
self,
username: Optional[str],
password: Optional[str],
resolver: AsyncResolver,
):
self._username = username
self._password = password
self._resolver = resolver
async def connect(
self,
stream: AsyncSocketStream,
host: str,
port: int,
) -> http.ConnectReply:
conn = http.Connection()
request = http.ConnectRequest(
host=host,
port=port,
username=self._username,
password=self._password,
)
data = conn.send(request)
await stream.write_all(data)
data = await stream.read()
reply: http.ConnectReply = conn.receive(data)
return reply

View File

@@ -0,0 +1,38 @@
from typing import Optional
from .._abc import SyncSocketStream, SyncResolver
from .abc import SyncConnector
from .._protocols import http
class HttpSyncConnector(SyncConnector):
def __init__(
self,
username: Optional[str],
password: Optional[str],
resolver: SyncResolver,
):
self._username = username
self._password = password
self._resolver = resolver
def connect(
self,
stream: SyncSocketStream,
host: str,
port: int,
) -> http.ConnectReply:
conn = http.Connection()
request = http.ConnectRequest(
host=host,
port=port,
username=self._username,
password=self._password,
)
data = conn.send(request)
stream.write_all(data)
data = stream.read()
reply: http.ConnectReply = conn.receive(data)
return reply

View File

@@ -0,0 +1,45 @@
import socket
from typing import Optional
from .._abc import AsyncSocketStream, AsyncResolver
from .abc import AsyncConnector
from .._protocols import socks4
from .._helpers import is_ip_address
class Socks4AsyncConnector(AsyncConnector):
def __init__(
self,
user_id: Optional[str],
rdns: Optional[bool],
resolver: AsyncResolver,
):
if rdns is None:
rdns = False
self._user_id = user_id
self._rdns = rdns
self._resolver = resolver
async def connect(
self,
stream: AsyncSocketStream,
host: str,
port: int,
) -> socks4.ConnectReply:
conn = socks4.Connection()
if not is_ip_address(host) and not self._rdns:
_, host = await self._resolver.resolve(
host,
family=socket.AF_INET,
)
request = socks4.ConnectRequest(host=host, port=port, user_id=self._user_id)
data = conn.send(request)
await stream.write_all(data)
data = await stream.read_exact(socks4.ConnectReply.SIZE)
reply: socks4.ConnectReply = conn.receive(data)
return reply

View File

@@ -0,0 +1,45 @@
import socket
from typing import Optional
from .._abc import SyncSocketStream, SyncResolver
from .abc import SyncConnector
from .._protocols import socks4
from .._helpers import is_ip_address
class Socks4SyncConnector(SyncConnector):
def __init__(
self,
user_id: Optional[str],
rdns: Optional[bool],
resolver: SyncResolver,
):
if rdns is None:
rdns = False
self._user_id = user_id
self._rdns = rdns
self._resolver = resolver
def connect(
self,
stream: SyncSocketStream,
host: str,
port: int,
) -> socks4.ConnectReply:
conn = socks4.Connection()
if not is_ip_address(host) and not self._rdns:
_, host = self._resolver.resolve(
host,
family=socket.AF_INET,
)
request = socks4.ConnectRequest(host=host, port=port, user_id=self._user_id)
data = conn.send(request)
stream.write_all(data)
data = stream.read_exact(socks4.ConnectReply.SIZE)
reply: socks4.ConnectReply = conn.receive(data)
return reply

View File

@@ -0,0 +1,95 @@
import socket
from typing import Optional
from .._abc import AsyncSocketStream, AsyncResolver
from .abc import AsyncConnector
from .._protocols import socks5
from .._helpers import is_ip_address
class Socks5AsyncConnector(AsyncConnector):
def __init__(
self,
username: Optional[str],
password: Optional[str],
rdns: Optional[bool],
resolver: AsyncResolver,
):
if rdns is None:
rdns = True
self._username = username
self._password = password
self._rdns = rdns
self._resolver = resolver
async def connect(
self,
stream: AsyncSocketStream,
host: str,
port: int,
) -> socks5.ConnectReply:
conn = socks5.Connection()
# Auth methods
request = socks5.AuthMethodsRequest(
username=self._username,
password=self._password,
)
data = conn.send(request)
await stream.write_all(data)
data = await stream.read_exact(socks5.AuthMethodReply.SIZE)
reply: socks5.AuthMethodReply = conn.receive(data)
# Authenticate
if reply.method == socks5.AuthMethod.USERNAME_PASSWORD:
request = socks5.AuthRequest(
username=self._username,
password=self._password,
)
data = conn.send(request)
await stream.write_all(data)
data = await stream.read_exact(socks5.AuthReply.SIZE)
_: socks5.AuthReply = conn.receive(data)
# Connect
if not is_ip_address(host) and not self._rdns:
_, host = await self._resolver.resolve(
host,
family=socket.AF_UNSPEC,
)
request = socks5.ConnectRequest(host=host, port=port)
data = conn.send(request)
await stream.write_all(data)
data = await self._read_reply(stream)
reply: socks5.ConnectReply = conn.receive(data)
return reply
# noinspection PyMethodMayBeStatic
async def _read_reply(self, stream: AsyncSocketStream) -> bytes:
data = await stream.read_exact(3)
if data[0] != socks5.SOCKS_VER:
return data
if data[1] != socks5.ReplyCode.SUCCEEDED:
return data
if data[2] != socks5.RSV:
return data
data += await stream.read_exact(1)
addr_type = data[3]
if addr_type == socks5.AddressType.IPV4:
data += await stream.read_exact(6)
elif addr_type == socks5.AddressType.IPV6:
data += await stream.read_exact(18)
elif addr_type == socks5.AddressType.DOMAIN:
data += await stream.read_exact(1)
host_len = data[-1]
data += await stream.read_exact(host_len + 2)
return data

View File

@@ -0,0 +1,86 @@
import socket
from typing import Optional
from .._abc import SyncSocketStream, SyncResolver
from .abc import SyncConnector
from .._protocols import socks5
from .._helpers import is_ip_address
class Socks5SyncConnector(SyncConnector):
def __init__(
self,
username: Optional[str],
password: Optional[str],
rdns: Optional[bool],
resolver: SyncResolver,
):
if rdns is None:
rdns = True
self._username = username
self._password = password
self._rdns = rdns
self._resolver = resolver
def connect(
self,
stream: SyncSocketStream,
host: str,
port: int,
) -> socks5.ConnectReply:
conn = socks5.Connection()
# Auth methods
request = socks5.AuthMethodsRequest(username=self._username, password=self._password)
data = conn.send(request)
stream.write_all(data)
data = stream.read_exact(socks5.AuthMethodReply.SIZE)
reply: socks5.AuthMethodReply = conn.receive(data)
# Authenticate
if reply.method == socks5.AuthMethod.USERNAME_PASSWORD:
request = socks5.AuthRequest(username=self._username, password=self._password)
data = conn.send(request)
stream.write_all(data)
data = stream.read_exact(socks5.AuthReply.SIZE)
_: socks5.AuthReply = conn.receive(data)
# Connect
if not is_ip_address(host) and not self._rdns:
_, host = self._resolver.resolve(host, family=socket.AF_UNSPEC)
request = socks5.ConnectRequest(host=host, port=port)
data = conn.send(request)
stream.write_all(data)
data = self._read_reply(stream)
reply: socks5.ConnectReply = conn.receive(data)
return reply
# noinspection PyMethodMayBeStatic
def _read_reply(self, stream: SyncSocketStream) -> bytes:
data = stream.read_exact(3)
if data[0] != socks5.SOCKS_VER:
return data
if data[1] != socks5.ReplyCode.SUCCEEDED:
return data
if data[2] != socks5.RSV:
return data
data += stream.read_exact(1)
addr_type = data[3]
if addr_type == socks5.AddressType.IPV4:
data += stream.read_exact(6)
elif addr_type == socks5.AddressType.IPV6:
data += stream.read_exact(18)
elif addr_type == socks5.AddressType.DOMAIN:
data += stream.read_exact(1)
host_len = data[-1]
data += stream.read_exact(host_len + 2)
return data

View File

@@ -0,0 +1,16 @@
class ProxyException(Exception):
pass
class ProxyTimeoutError(ProxyException, TimeoutError):
pass
class ProxyConnectionError(ProxyException, OSError):
pass
class ProxyError(ProxyException):
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code

View File

@@ -0,0 +1,81 @@
import functools
import re
from typing import Optional, Tuple
from urllib.parse import urlparse, unquote
from ._types import ProxyType
# pylint:disable-next=invalid-name
_ipv4_pattern = (
r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
)
# pylint:disable-next=invalid-name
_ipv6_pattern = (
r'^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}'
r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)'
r'((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})'
r'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}'
r'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}'
r'[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)'
r'(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}'
r':|:(:[A-F0-9]{1,4}){7})$'
)
_ipv4_regex = re.compile(_ipv4_pattern)
_ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE)
_ipv4_regexb = re.compile(_ipv4_pattern.encode('ascii'))
_ipv6_regexb = re.compile(_ipv6_pattern.encode('ascii'), flags=re.IGNORECASE)
def _is_ip_address(regex, regexb, host):
# if host is None:
# return False
if isinstance(host, str):
return bool(regex.match(host))
elif isinstance(host, (bytes, bytearray, memoryview)):
return bool(regexb.match(host))
else:
raise TypeError(
'{} [{}] is not a str or bytes'.format(host, type(host)) # pragma: no cover
)
is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb)
is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb)
def is_ip_address(host):
return is_ipv4_address(host) or is_ipv6_address(host)
def parse_proxy_url(url: str) -> Tuple[ProxyType, str, int, Optional[str], Optional[str]]:
parsed = urlparse(url)
scheme = parsed.scheme
if scheme == 'socks5':
proxy_type = ProxyType.SOCKS5
elif scheme == 'socks4':
proxy_type = ProxyType.SOCKS4
elif scheme == 'http':
proxy_type = ProxyType.HTTP
else:
raise ValueError(f'Invalid scheme component: {scheme}') # pragma: no cover
host = parsed.hostname
if not host:
raise ValueError('Empty host component') # pragma: no cover
try:
port = parsed.port
assert port is not None
except (ValueError, TypeError, AssertionError) as e: # pragma: no cover
raise ValueError('Invalid port component') from e
try:
username, password = (unquote(parsed.username), unquote(parsed.password))
except (AttributeError, TypeError):
username, password = '', ''
return proxy_type, host, port, username, password

View File

@@ -0,0 +1,4 @@
class ReplyError(Exception):
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code

View File

@@ -0,0 +1,148 @@
import sys
from dataclasses import dataclass
import base64
import binascii
from collections import namedtuple
from typing import Optional
from .._version import __title__, __version__
from .errors import ReplyError
DEFAULT_USER_AGENT = 'Python/{0[0]}.{0[1]} {1}/{2}'.format(
sys.version_info,
__title__,
__version__,
)
CRLF = '\r\n'
class BasicAuth(namedtuple('BasicAuth', ['login', 'password', 'encoding'])):
"""Http basic authentication helper."""
def __new__(cls, login: str, password: str = '', encoding: str = 'latin1') -> 'BasicAuth':
if login is None:
raise ValueError('None is not allowed as login value')
if password is None:
raise ValueError('None is not allowed as password value')
if ':' in login:
raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
# noinspection PyTypeChecker,PyArgumentList
return super().__new__(cls, login, password, encoding)
@classmethod
def decode(cls, auth_header: str, encoding: str = 'latin1') -> 'BasicAuth':
"""Create a BasicAuth object from an Authorization HTTP header."""
try:
auth_type, encoded_credentials = auth_header.split(' ', 1)
except ValueError:
raise ValueError('Could not parse authorization header.')
if auth_type.lower() != 'basic':
raise ValueError('Unknown authorization method %s' % auth_type)
try:
decoded = base64.b64decode(encoded_credentials.encode('ascii'), validate=True).decode(
encoding
)
except binascii.Error:
raise ValueError('Invalid base64 encoding.')
try:
# RFC 2617 HTTP Authentication
# https://www.ietf.org/rfc/rfc2617.txt
# the colon must be present, but the username and password may be
# otherwise blank.
username, password = decoded.split(':', 1)
except ValueError:
raise ValueError('Invalid credentials.')
# noinspection PyTypeChecker
return cls(username, password, encoding=encoding)
def encode(self) -> str:
"""Encode credentials."""
creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding)
return 'Basic %s' % base64.b64encode(creds).decode(self.encoding)
class _Buffer:
def __init__(self, encoding: str = 'utf-8'):
self._encoding = encoding
self._buffer = bytearray()
def append_line(self, line: str = ""):
if line:
self._buffer.extend(line.encode(self._encoding))
self._buffer.extend(CRLF.encode('ascii'))
def dumps(self) -> bytes:
return bytes(self._buffer)
@dataclass
class ConnectRequest:
host: str
port: int
username: Optional[str]
password: Optional[str]
def dumps(self) -> bytes:
buff = _Buffer()
buff.append_line(f'CONNECT {self.host}:{self.port} HTTP/1.1')
buff.append_line(f'Host: {self.host}:{self.port}')
buff.append_line(f'User-Agent: {DEFAULT_USER_AGENT}')
if self.username and self.password:
auth = BasicAuth(self.username, self.password)
buff.append_line(f'Proxy-Authorization: {auth.encode()}')
buff.append_line()
return buff.dumps()
@dataclass
class ConnectReply:
status_code: int
message: str
@classmethod
def loads(cls, data: bytes) -> 'ConnectReply':
if not data:
raise ReplyError('Invalid proxy response') # pragma: no cover
line = data.split(CRLF.encode('ascii'), 1)[0]
line = line.decode('utf-8', 'surrogateescape')
try:
version, code, *reason = line.split()
except ValueError: # pragma: no cover
raise ReplyError(f'Invalid status line: {line}')
try:
status_code = int(code)
except ValueError: # pragma: no cover
raise ReplyError(f'Invalid status code: {code}')
status_message = " ".join(reason)
if status_code != 200:
msg = f'{status_code} {status_message}'
raise ReplyError(msg, error_code=status_code)
return cls(status_code=status_code, message=status_message)
# noinspection PyMethodMayBeStatic
class Connection:
def send(self, request: ConnectRequest) -> bytes:
return request.dumps()
def receive(self, data: bytes) -> ConnectReply:
return ConnectReply.loads(data)

View File

@@ -0,0 +1,116 @@
import enum
import ipaddress
import socket
from dataclasses import dataclass
from typing import Optional
from .errors import ReplyError
from .._helpers import is_ipv4_address
RSV = NULL = 0x00
SOCKS_VER = 0x04
class Command(enum.IntEnum):
CONNECT = 0x01
BIND = 0x02
class ReplyCode(enum.IntEnum):
REQUEST_GRANTED = 0x5A
REQUEST_REJECTED_OR_FAILED = 0x5B
CONNECTION_FAILED = 0x5C
AUTHENTICATION_FAILED = 0x5D
ReplyMessages = {
ReplyCode.REQUEST_GRANTED: 'Request granted',
ReplyCode.REQUEST_REJECTED_OR_FAILED: 'Request rejected or failed',
ReplyCode.CONNECTION_FAILED: (
'Request rejected because SOCKS server cannot connect to identd on the client'
),
ReplyCode.AUTHENTICATION_FAILED: (
'Request rejected because the client program and identd report different user-ids'
),
}
@dataclass
class ConnectRequest:
host: str # hostname or IPv4 address
port: int
user_id: Optional[str]
def dumps(self):
port_bytes = self.port.to_bytes(2, 'big')
include_hostname = False
if is_ipv4_address(self.host):
host_bytes = ipaddress.IPv4Address(self.host).packed
else:
include_hostname = True
host_bytes = bytes([NULL, NULL, NULL, 1])
data = bytearray([SOCKS_VER, Command.CONNECT])
data += port_bytes
data += host_bytes
if self.user_id:
data += self.user_id.encode('ascii')
data.append(NULL)
if include_hostname:
data += self.host.encode('idna')
data.append(NULL)
return bytes(data)
@dataclass
class ConnectReply:
SIZE = 8
rsv: int
reply: ReplyCode
host: str # should be ignored when using Command.CONNECT
port: int # should be ignored when using Command.CONNECT
@classmethod
def loads(cls, data: bytes) -> 'ConnectReply':
if len(data) != cls.SIZE:
raise ReplyError('Malformed connect reply')
rsv = data[0]
if rsv != RSV: # pragma: no cover
raise ReplyError(f'Unexpected reply version: {data[0]:#02X}')
try:
reply = ReplyCode(data[1])
except ValueError:
raise ReplyError(f'Invalid reply code: {data[1]:#02X}')
if reply != ReplyCode.REQUEST_GRANTED: # pragma: no cover
msg = ReplyMessages.get(reply, 'Unknown error')
raise ReplyError(msg, error_code=reply)
try:
port = int.from_bytes(data[2:4], byteorder="big")
except ValueError:
raise ReplyError('Invalid port data')
try:
host = socket.inet_ntop(socket.AF_INET, data[4:8])
except ValueError:
raise ReplyError('Invalid port data')
return cls(rsv=rsv, reply=reply, host=host, port=port)
# noinspection PyMethodMayBeStatic
class Connection:
def send(self, request: ConnectRequest) -> bytes:
return request.dumps()
def receive(self, data: bytes) -> ConnectReply:
return ConnectReply.loads(data)

View File

@@ -0,0 +1,355 @@
import enum
import ipaddress
import socket
from typing import Optional, Union
from dataclasses import dataclass, field
from .errors import ReplyError
from .._helpers import is_ip_address
RSV = NULL = AUTH_GRANTED = 0x00
SOCKS_VER = 0x05
class AuthMethod(enum.IntEnum):
ANONYMOUS = 0x00
GSSAPI = 0x01
USERNAME_PASSWORD = 0x02
NO_ACCEPTABLE = 0xFF
class AddressType(enum.IntEnum):
IPV4 = 0x01
DOMAIN = 0x03
IPV6 = 0x04
@classmethod
def from_ip_ver(cls, ver: int):
if ver == 4:
return cls.IPV4
if ver == 6:
return cls.IPV6
raise ValueError('Invalid IP version')
class Command(enum.IntEnum):
CONNECT = 0x01
BIND = 0x02
UDP_ASSOCIATE = 0x03
class ReplyCode(enum.IntEnum):
SUCCEEDED = 0x00
GENERAL_FAILURE = 0x01
CONNECTION_NOT_ALLOWED = 0x02
NETWORK_UNREACHABLE = 0x03
HOST_UNREACHABLE = 0x04
CONNECTION_REFUSED = 0x05
TTL_EXPIRED = 0x06
COMMAND_NOT_SUPPORTED = 0x07
ADDRESS_TYPE_NOT_SUPPORTED = 0x08
ReplyMessages = {
ReplyCode.SUCCEEDED: 'Request granted',
ReplyCode.GENERAL_FAILURE: 'General SOCKS server failure',
ReplyCode.CONNECTION_NOT_ALLOWED: 'Connection not allowed by ruleset',
ReplyCode.NETWORK_UNREACHABLE: 'Network unreachable',
ReplyCode.HOST_UNREACHABLE: 'Host unreachable',
ReplyCode.CONNECTION_REFUSED: 'Connection refused by destination host',
ReplyCode.TTL_EXPIRED: 'TTL expired',
ReplyCode.COMMAND_NOT_SUPPORTED: 'Command not supported or protocol error',
ReplyCode.ADDRESS_TYPE_NOT_SUPPORTED: 'Address type not supported',
}
@dataclass
class AuthMethodsRequest:
username: Optional[str]
password: Optional[str]
methods: bytearray = field(init=False)
def __post_init__(self):
methods = bytearray([AuthMethod.ANONYMOUS])
if self.username and self.password:
methods.append(AuthMethod.USERNAME_PASSWORD)
self.methods = methods
def dumps(self) -> bytes:
return bytes([SOCKS_VER, len(self.methods)]) + self.methods
@dataclass
class AuthMethodReply:
SIZE = 2
ver: int
method: AuthMethod
def validate(self, request: AuthMethodsRequest):
if self.method not in request.methods: # pragma: no cover
raise ReplyError(f'Unexpected SOCKS authentication method: {self.method}')
@classmethod
def loads(cls, data: bytes) -> 'AuthMethodReply':
if len(data) != cls.SIZE:
raise ReplyError('Malformed authentication method reply')
ver = data[0]
if ver != SOCKS_VER: # pragma: no cover
raise ReplyError(f'Unexpected SOCKS version number: {ver}')
try:
method = AuthMethod(data[1])
except ValueError:
raise ReplyError(f'Invalid authentication method: {data[1]:#02X}')
if method == AuthMethod.NO_ACCEPTABLE: # pragma: no cover
raise ReplyError('No acceptable authentication methods were offered')
return cls(ver=ver, method=method)
@dataclass
class AuthRequest:
VER = 0x01
username: str
password: str
def dumps(self) -> bytes:
data = bytearray()
data.append(self.VER)
data.append(len(self.username))
data += self.username.encode('ascii')
data.append(len(self.password))
data += self.password.encode('ascii')
return bytes(data)
@dataclass
class AuthReply:
SIZE = 2
ver: int
status: int
@classmethod
def loads(cls, data: bytes) -> 'AuthReply':
if len(data) != cls.SIZE:
raise ReplyError('Malformed auth reply')
ver = data[0]
if ver != AuthRequest.VER: # pragma: no cover
raise ReplyError('Invalid authentication response')
status = data[1]
if status != AUTH_GRANTED: # pragma: no cover
raise ReplyError('Username and password authentication failure')
return cls(ver=ver, status=status)
@dataclass
class ConnectRequest:
host: str # hostname or IPv4 or IPv6 address
port: int
def dumps(self) -> bytes:
data = bytearray([SOCKS_VER, Command.CONNECT, RSV])
data += self._build_addr_request()
return bytes(data)
def _build_addr_request(self) -> bytes:
port = self.port.to_bytes(2, 'big')
if is_ip_address(self.host):
ip = ipaddress.ip_address(self.host)
address_type = AddressType.from_ip_ver(ip.version)
return bytes([address_type]) + ip.packed + port
else:
address_type = AddressType.DOMAIN
host = self.host.encode('idna')
return bytes([address_type, len(host)]) + host + port
@dataclass
class ConnectReply:
ver: int
reply: ReplyCode
rsv: int
bound_host: str
bound_port: int
def validate(self):
pass
@classmethod
def loads(cls, data: bytes) -> 'ConnectReply':
if not data:
raise ReplyError('Empty connect reply')
ver = data[0]
if ver != SOCKS_VER: # pragma: no cover
raise ReplyError(f'Unexpected SOCKS version number: {ver:#02X}')
try:
reply = ReplyCode(data[1])
except IndexError:
raise ReplyError('Malformed connect reply')
except ValueError:
raise ReplyError(f'Invalid reply code: {data[1]:#02X}')
if reply != ReplyCode.SUCCEEDED: # pragma: no cover
msg = ReplyMessages.get(reply, 'Unknown error') # type: ignore
raise ReplyError(msg, error_code=reply)
try:
rsv = data[2]
except IndexError:
raise ReplyError('Malformed connect reply')
if rsv != RSV: # pragma: no cover
raise ReplyError(f'The reserved byte must be {RSV:#02X}')
try:
addr_type = data[3]
bnd_host_data = data[4:-2]
bnd_port_data = data[-2:]
except IndexError:
raise ReplyError('Malformed connect reply')
if addr_type == AddressType.IPV4:
bnd_host = socket.inet_ntop(socket.AF_INET, bnd_host_data)
elif addr_type == AddressType.IPV6:
bnd_host = socket.inet_ntop(socket.AF_INET6, bnd_host_data)
elif addr_type == AddressType.DOMAIN: # pragma: no cover
# host_len = bnd_host_data[0]
bnd_host = bnd_host_data[1:].decode()
else: # pragma: no cover
raise ReplyError(f'Invalid address type: {addr_type:#02X}')
bnd_port = int.from_bytes(bnd_port_data, 'big')
return cls(
ver=ver,
reply=reply,
rsv=rsv,
bound_host=bnd_host,
bound_port=bnd_port,
)
class StateServerWaitingForAuthMethods:
pass
@dataclass
class StateClientSentAuthMethods:
data: AuthMethodsRequest
@dataclass
class StateServerWaitingForAuth:
data: AuthMethodReply
@dataclass
class StateClientAuthenticated:
data: Optional[AuthReply] = None
@dataclass
class StateClientSentAuthRequest:
data: AuthRequest
@dataclass
class StateClientSentConnectRequest:
data: ConnectRequest
@dataclass
class StateServerConnected:
data: ConnectReply
Request = Union[
AuthMethodsRequest,
AuthRequest,
ConnectRequest,
]
Reply = Union[
AuthMethodReply,
AuthReply,
ConnectReply,
]
ConnectionState = Union[
StateServerWaitingForAuthMethods,
StateClientSentAuthMethods,
StateServerWaitingForAuth,
StateClientSentAuthRequest,
StateClientAuthenticated,
StateClientSentConnectRequest,
StateServerConnected,
]
class Connection:
_state: ConnectionState
def __init__(self):
self._state = StateServerWaitingForAuthMethods()
def send(self, request: Request) -> bytes:
if type(request) is AuthMethodsRequest:
if type(self._state) is not StateServerWaitingForAuthMethods:
raise RuntimeError('Server is not currently waiting for auth methods')
self._state = StateClientSentAuthMethods(request)
return request.dumps()
if type(request) is AuthRequest:
if type(self._state) is not StateServerWaitingForAuth:
raise RuntimeError('Server is not currently waiting for authentication')
self._state = StateClientSentAuthRequest(request)
return request.dumps()
if type(request) is ConnectRequest:
if type(self._state) is not StateClientAuthenticated:
raise RuntimeError('Client is not authenticated')
self._state = StateClientSentConnectRequest(request)
return request.dumps()
raise RuntimeError(f'Invalid request type: {type(request)}')
def receive(self, data: bytes) -> Reply:
if type(self._state) is StateClientSentAuthMethods:
reply = AuthMethodReply.loads(data)
reply.validate(self._state.data)
if reply.method == AuthMethod.USERNAME_PASSWORD:
self._state = StateServerWaitingForAuth(data=reply)
else:
self._state = StateClientAuthenticated()
return reply
if type(self._state) is StateClientSentAuthRequest:
reply = AuthReply.loads(data)
self._state = StateClientAuthenticated(data=reply)
return reply
if type(self._state) is StateClientSentConnectRequest:
reply = ConnectReply.loads(data)
self._state = StateServerConnected(data=reply)
return reply
raise RuntimeError(f'Invalid connection state: {self._state}')
@property
def state(self):
return self._state

View File

@@ -0,0 +1,7 @@
from enum import Enum
class ProxyType(Enum):
SOCKS4 = 1
SOCKS5 = 2
HTTP = 3

View File

@@ -0,0 +1,2 @@
__title__ = 'python-socks'
__version__ = '2.8.1'

View File

@@ -0,0 +1,3 @@
from ._proxy_chain import ProxyChain
__all__ = ('ProxyChain',)

View File

@@ -0,0 +1,34 @@
from typing import Iterable
import warnings
class ProxyChain:
def __init__(self, proxies: Iterable):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
async def connect(self, dest_host, dest_port, timeout=None):
curr_socket = None
proxies = list(self._proxies)
length = len(proxies) - 1
for i in range(length):
curr_socket = await proxies[i].connect(
dest_host=proxies[i + 1].proxy_host,
dest_port=proxies[i + 1].proxy_port,
timeout=timeout,
_socket=curr_socket,
)
curr_socket = await proxies[length].connect(
dest_host=dest_host,
dest_port=dest_port,
timeout=timeout,
_socket=curr_socket,
)
return curr_socket

View File

@@ -0,0 +1,4 @@
from ._proxy import AnyioProxy as Proxy
from ._chain import ProxyChain
__all__ = ('Proxy', 'ProxyChain')

View File

@@ -0,0 +1,42 @@
from typing import Iterable
import warnings
from ._proxy import AnyioProxy
class ProxyChain:
def __init__(self, proxies: Iterable[AnyioProxy]):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
async def connect(
self,
dest_host,
dest_port,
dest_ssl=None,
timeout=None,
):
_stream = None
proxies = list(self._proxies)
length = len(proxies) - 1
for i in range(length):
_stream = await proxies[i].connect(
dest_host=proxies[i + 1].proxy_host,
dest_port=proxies[i + 1].proxy_port,
timeout=timeout,
_stream=_stream,
)
_stream = await proxies[length].connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
timeout=timeout,
_stream=_stream,
)
return _stream

View File

@@ -0,0 +1,16 @@
from typing import Optional
import anyio
import anyio.abc
async def connect_tcp(
host: str,
port: int,
local_host: Optional[str] = None,
) -> anyio.abc.SocketStream:
return await anyio.connect_tcp(
remote_host=host,
remote_port=port,
local_host=local_host,
)

View File

@@ -0,0 +1,137 @@
import ssl
from typing import Any, Optional
import warnings
import anyio
from ..._types import ProxyType
from ..._helpers import parse_proxy_url
from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ._resolver import Resolver
from ._stream import AnyioSocketStream
from ._connect import connect_tcp
from ..._protocols.errors import ReplyError
from ..._connectors.factory_async import create_connector
DEFAULT_TIMEOUT = 60
class AnyioProxy:
_stream: Optional[AnyioSocketStream]
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
proxy_ssl: Optional[ssl.SSLContext] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._password = password
self._username = username
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._resolver = Resolver()
async def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
timeout: Optional[float] = None,
**kwargs: Any,
) -> AnyioSocketStream:
if timeout is None:
timeout = DEFAULT_TIMEOUT
_stream = kwargs.get('_stream')
if _stream is not None:
warnings.warn(
"The '_stream' argument is deprecated and will be removed in the future",
DeprecationWarning,
stacklevel=2,
)
local_host = kwargs.get('local_host')
try:
with anyio.fail_after(timeout):
if _stream is None:
try:
_stream = AnyioSocketStream(
await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
local_host=local_host,
)
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
stream = _stream
try:
if self._proxy_ssl is not None:
stream = await stream.start_tls(
hostname=self._proxy_host,
ssl_context=self._proxy_ssl,
)
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
if dest_ssl is not None:
stream = await stream.start_tls(
hostname=dest_host,
ssl_context=dest_ssl,
)
return stream
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except BaseException:
await stream.close()
raise
except TimeoutError as e:
raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e
@property
def proxy_host(self):
return self._proxy_host
@property
def proxy_port(self):
return self._proxy_port
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'AnyioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,22 @@
import anyio
import socket
from ... import _abc as abc
class Resolver(abc.AsyncResolver):
async def resolve(self, host, port=0, family=socket.AF_UNSPEC):
infos = await anyio.getaddrinfo(
host=host,
port=port,
family=family,
type=socket.SOCK_STREAM,
)
if not infos: # pragma: no cover
raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family))
infos = sorted(infos, key=lambda info: info[0])
family, _, _, _, address = infos[0]
return family, address[0]

View File

@@ -0,0 +1,59 @@
import ssl
from typing import Union
import anyio
import anyio.abc
from anyio.streams.tls import TLSStream
from ..._errors import ProxyError
from ... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
AnyioStreamType = Union[anyio.abc.SocketStream, TLSStream]
class AnyioSocketStream(abc.AsyncSocketStream):
_stream: AnyioStreamType
def __init__(self, stream: AnyioStreamType) -> None:
self._stream = stream
async def write_all(self, data: bytes):
await self._stream.send(item=data)
async def read(self, max_bytes: int = DEFAULT_RECEIVE_SIZE):
try:
return await self._stream.receive(max_bytes=max_bytes)
except anyio.EndOfStream: # pragma: no cover
return b""
async def read_exact(self, n: int):
data = bytearray()
while len(data) < n:
packet = await self.read(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def start_tls(
self,
hostname: str,
ssl_context: ssl.SSLContext,
) -> 'AnyioSocketStream':
ssl_stream = await TLSStream.wrap(
self._stream,
ssl_context=ssl_context,
hostname=hostname,
standard_compatible=False,
server_side=False,
)
return AnyioSocketStream(ssl_stream)
async def close(self):
await self._stream.aclose()
@property
def anyio_stream(self) -> AnyioStreamType: # pragma: no cover
return self._stream

View File

@@ -0,0 +1,7 @@
from ._proxy import AnyioProxy as Proxy
from ._chain import ProxyChain
__all__ = (
'Proxy',
'ProxyChain',
)

View File

@@ -0,0 +1,32 @@
from typing import Sequence
import warnings
from ._proxy import AnyioProxy
class ProxyChain:
def __init__(self, proxies: Sequence[AnyioProxy]):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
async def connect(
self,
dest_host,
dest_port,
dest_ssl=None,
timeout=None,
):
forward = None
for proxy in self._proxies:
proxy._forward = forward
forward = proxy
return await forward.connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
timeout=timeout,
)

View File

@@ -0,0 +1,17 @@
from typing import Optional
import anyio
import anyio.abc
from ._stream import AnyioSocketStream
async def connect_tcp(
host: str,
port: int,
local_host: Optional[str] = None,
) -> AnyioSocketStream:
s = await anyio.connect_tcp(
remote_host=host,
remote_port=port,
local_host=local_host,
)
return AnyioSocketStream(s)

View File

@@ -0,0 +1,135 @@
import ssl
from typing import Any, Optional
import anyio
from ._connect import connect_tcp
from ._stream import AnyioSocketStream
from .._resolver import Resolver
from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ...._types import ProxyType
from ...._helpers import parse_proxy_url
from ...._protocols.errors import ReplyError
from ...._connectors.factory_async import create_connector
DEFAULT_TIMEOUT = 60
class AnyioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
proxy_ssl: Optional[ssl.SSLContext] = None,
forward: Optional['AnyioProxy'] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._forward = forward
self._resolver = Resolver()
async def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
timeout: Optional[float] = None,
**kwargs: Any,
) -> AnyioSocketStream:
if timeout is None:
timeout = DEFAULT_TIMEOUT
local_host = kwargs.get('local_host')
try:
with anyio.fail_after(timeout):
return await self._connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
local_host=local_host,
)
except TimeoutError as e:
raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e
async def _connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
local_host: Optional[str] = None,
) -> AnyioSocketStream:
if self._forward is None:
try:
stream = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
local_host=local_host,
)
except OSError as e:
raise ProxyConnectionError(
e.errno,
"Couldn't connect to proxy"
f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]",
) from e
else:
stream = await self._forward.connect(
dest_host=self._proxy_host,
dest_port=self._proxy_port,
)
try:
if self._proxy_ssl is not None:
stream = await stream.start_tls(
hostname=self._proxy_host,
ssl_context=self._proxy_ssl,
)
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
if dest_ssl is not None:
stream = await stream.start_tls(
hostname=dest_host,
ssl_context=dest_ssl,
)
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except BaseException:
with anyio.CancelScope(shield=True):
await stream.close()
raise
return stream
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'AnyioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,59 @@
import ssl
from typing import Union
import anyio
import anyio.abc
from anyio.streams.tls import TLSStream
from ...._errors import ProxyError
from .... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
AnyioStreamType = Union[anyio.abc.SocketStream, TLSStream]
class AnyioSocketStream(abc.AsyncSocketStream):
_stream: AnyioStreamType
def __init__(self, stream: AnyioStreamType) -> None:
self._stream = stream
async def write_all(self, data: bytes):
await self._stream.send(item=data)
async def read(self, max_bytes: int = DEFAULT_RECEIVE_SIZE):
try:
return await self._stream.receive(max_bytes=max_bytes)
except anyio.EndOfStream: # pragma: no cover
return b""
async def read_exact(self, n: int):
data = bytearray()
while len(data) < n:
packet = await self.read(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def start_tls(
self,
hostname: str,
ssl_context: ssl.SSLContext,
) -> 'AnyioSocketStream':
ssl_stream = await TLSStream.wrap(
self._stream,
ssl_context=ssl_context,
hostname=hostname,
standard_compatible=False,
server_side=False,
)
return AnyioSocketStream(ssl_stream)
async def close(self):
await self._stream.aclose()
@property
def anyio_stream(self) -> AnyioStreamType: # pragma: no cover
return self._stream

View File

@@ -0,0 +1,4 @@
from ._proxy import AsyncioProxy as Proxy
__all__ = ('Proxy',)

View File

@@ -0,0 +1,43 @@
import socket
import asyncio
from typing import Optional, Tuple
from ._resolver import Resolver
from ..._helpers import is_ipv4_address, is_ipv6_address
async def connect_tcp(
host: str,
port: int,
loop: asyncio.AbstractEventLoop,
local_addr: Optional[Tuple[str, int]] = None,
) -> socket.socket:
family, host = await _resolve_host(host, loop)
sock = socket.socket(family=family, type=socket.SOCK_STREAM)
sock.setblocking(False)
if local_addr is not None: # pragma: no cover
sock.bind(local_addr)
if is_ipv6_address(host):
address = (host, port, 0, 0) # to fix OSError: [WinError 10022]
else:
address = (host, port) # type: ignore[assignment]
try:
await loop.sock_connect(sock=sock, address=address)
except OSError:
sock.close()
raise
return sock
async def _resolve_host(host, loop):
if is_ipv4_address(host):
return socket.AF_INET, host
if is_ipv6_address(host):
return socket.AF_INET6, host
resolver = Resolver(loop=loop)
return await resolver.resolve(host=host)

View File

@@ -0,0 +1,143 @@
import asyncio
import socket
import sys
from typing import Any, Optional
import warnings
from ..._types import ProxyType
from ..._helpers import parse_proxy_url
from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ._stream import AsyncioSocketStream
from ._resolver import Resolver
from ..._protocols.errors import ReplyError
from ..._connectors.factory_async import create_connector
from ._connect import connect_tcp
if sys.version_info >= (3, 11):
import asyncio as async_timeout # pylint:disable=reimported
else:
import async_timeout
DEFAULT_TIMEOUT = 60
class AsyncioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
if loop is None:
loop = asyncio.get_event_loop()
self._loop = loop
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._password = password
self._username = username
self._rdns = rdns
self._resolver = Resolver(loop=loop)
async def connect(
self,
dest_host: str,
dest_port: int,
timeout: Optional[float] = None,
**kwargs: Any,
) -> socket.socket:
if timeout is None:
timeout = DEFAULT_TIMEOUT
_socket = kwargs.get('_socket')
if _socket is not None:
warnings.warn(
"The '_socket' argument is deprecated and will be removed in the future",
DeprecationWarning,
stacklevel=2,
)
local_addr = kwargs.get('local_addr')
try:
async with async_timeout.timeout(timeout):
return await self._connect(
dest_host=dest_host,
dest_port=dest_port,
_socket=_socket,
local_addr=local_addr,
)
except asyncio.TimeoutError as e:
raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e
async def _connect(
self,
dest_host,
dest_port,
_socket=None,
local_addr=None,
) -> socket.socket:
if _socket is None:
try:
_socket = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
loop=self._loop,
local_addr=local_addr,
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
stream = AsyncioSocketStream(sock=_socket, loop=self._loop)
try:
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
return _socket
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except (asyncio.CancelledError, Exception): # pragma: no cover
await stream.close()
raise
@property
def proxy_host(self):
return self._proxy_host
@property
def proxy_port(self):
return self._proxy_port
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'AsyncioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,25 @@
import asyncio
import socket
from ... import _abc as abc
class Resolver(abc.AsyncResolver):
def __init__(self, loop: asyncio.AbstractEventLoop):
self._loop = loop
async def resolve(self, host, port=0, family=socket.AF_UNSPEC):
infos = await self._loop.getaddrinfo(
host=host,
port=port,
family=family,
type=socket.SOCK_STREAM,
)
if not infos: # pragma: no cover
raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family))
infos = sorted(infos, key=lambda info: info[0])
family, _, _, _, address = infos[0]
return family, address[0]

View File

@@ -0,0 +1,36 @@
import asyncio
import socket
from ..._errors import ProxyError
from ... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
class AsyncioSocketStream(abc.AsyncSocketStream):
_loop: asyncio.AbstractEventLoop = None
_socket = None
def __init__(self, sock: socket.socket, loop: asyncio.AbstractEventLoop):
self._loop = loop
self._socket = sock
async def write_all(self, data):
await self._loop.sock_sendall(self._socket, data)
async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return await self._loop.sock_recv(self._socket, max_bytes)
async def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = await self._loop.sock_recv(self._socket, n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def close(self):
if self._socket is not None:
self._socket.close()

View File

@@ -0,0 +1,4 @@
from ._proxy import AsyncioProxy as Proxy
from ._chain import ProxyChain
__all__ = ('Proxy', 'ProxyChain')

View File

@@ -0,0 +1,32 @@
from typing import Sequence
import warnings
from ._proxy import AsyncioProxy
class ProxyChain:
def __init__(self, proxies: Sequence[AsyncioProxy]):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
async def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl=None,
timeout=None,
):
forward = None
for proxy in self._proxies:
proxy._forward = forward
forward = proxy
return await forward.connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
timeout=timeout,
)

View File

@@ -0,0 +1,26 @@
import asyncio
from typing import Optional, Tuple
from ._stream import AsyncioSocketStream
async def connect_tcp(
host: str,
port: int,
loop: asyncio.AbstractEventLoop,
local_addr: Optional[Tuple[str, int]] = None,
) -> AsyncioSocketStream:
kwargs = {}
if local_addr is not None:
kwargs['local_addr'] = local_addr # pragma: no cover
reader, writer = await asyncio.open_connection(
host=host,
port=port,
**kwargs, # type: ignore
)
return AsyncioSocketStream(
loop=loop,
reader=reader,
writer=writer,
)

View File

@@ -0,0 +1,157 @@
import asyncio
import ssl
from typing import Any, Optional, Tuple
import warnings
import sys
from ...._types import ProxyType
from ...._helpers import parse_proxy_url
from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ...._protocols.errors import ReplyError
from ...._connectors.factory_async import create_connector
from .._resolver import Resolver
from ._stream import AsyncioSocketStream
from ._connect import connect_tcp
if sys.version_info >= (3, 11):
import asyncio as async_timeout # pylint:disable=reimported
else:
import async_timeout
DEFAULT_TIMEOUT = 60
class AsyncioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
proxy_ssl: Optional[ssl.SSLContext] = None,
forward: Optional['AsyncioProxy'] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
):
if loop is not None: # pragma: no cover
warnings.warn(
'The loop argument is deprecated and scheduled for removal in the future.',
DeprecationWarning,
stacklevel=2,
)
if loop is None:
loop = asyncio.get_event_loop()
self._loop = loop
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._forward = forward
self._resolver = Resolver(loop=loop)
async def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
timeout: Optional[float] = None,
**kwargs: Any,
) -> AsyncioSocketStream:
if timeout is None:
timeout = DEFAULT_TIMEOUT
local_addr = kwargs.get('local_addr')
try:
async with async_timeout.timeout(timeout):
return await self._connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
local_addr=local_addr,
)
except asyncio.TimeoutError as e:
raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e
async def _connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
local_addr: Optional[Tuple[str, int]] = None,
) -> AsyncioSocketStream:
if self._forward is None:
try:
stream = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
loop=self._loop,
local_addr=local_addr,
)
except OSError as e:
raise ProxyConnectionError(
e.errno,
"Couldn't connect to proxy"
f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]",
) from e
else:
stream = await self._forward.connect(
dest_host=self._proxy_host,
dest_port=self._proxy_port,
)
try:
if self._proxy_ssl is not None:
stream = await stream.start_tls(
hostname=self._proxy_host,
ssl_context=self._proxy_ssl,
)
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
if dest_ssl is not None:
stream = await stream.start_tls(
hostname=dest_host,
ssl_context=dest_ssl,
)
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except (asyncio.CancelledError, Exception):
await stream.close()
raise
return stream
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'AsyncioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,91 @@
import asyncio
import ssl
from .... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
class AsyncioSocketStream(abc.AsyncSocketStream):
_loop: asyncio.AbstractEventLoop
_reader: asyncio.StreamReader
_writer: asyncio.StreamWriter
def __init__(
self,
loop: asyncio.AbstractEventLoop,
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
):
self._loop = loop
self._reader = reader
self._writer = writer
async def write_all(self, data):
self._writer.write(data)
await self._writer.drain()
async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return await self._reader.read(max_bytes)
async def read_exact(self, n):
return await self._reader.readexactly(n)
async def start_tls(
self,
hostname: str,
ssl_context: ssl.SSLContext,
ssl_handshake_timeout=None,
) -> 'AsyncioSocketStream':
if hasattr(self._writer, 'start_tls'): # Python>=3.11
await self._writer.start_tls(
ssl_context,
server_hostname=hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
)
return self
reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
transport: asyncio.Transport = await self._loop.start_tls(
self._writer.transport, # type: ignore
protocol,
ssl_context,
server_side=False,
server_hostname=hostname,
ssl_handshake_timeout=ssl_handshake_timeout,
)
# reader.set_transport(transport)
# Initialize the protocol, so it is made aware of being tied to
# a TLS connection.
# See: https://github.com/encode/httpx/issues/859
protocol.connection_made(transport)
writer = asyncio.StreamWriter(
transport=transport,
protocol=protocol,
reader=reader,
loop=self._loop,
)
stream = AsyncioSocketStream(loop=self._loop, reader=reader, writer=writer)
# When we return a new SocketStream with new StreamReader/StreamWriter instances
# we need to keep references to the old StreamReader/StreamWriter so that they
# are not garbage collected and closed while we're still using them.
stream._inner = self # type: ignore # pylint:disable=W0212,W0201
return stream
async def close(self):
self._writer.close()
self._writer.transport.abort() # noqa
@property
def reader(self):
return self._reader # pragma: no cover
@property
def writer(self):
return self._writer # pragma: no cover

View File

@@ -0,0 +1,4 @@
from ._proxy import CurioProxy as Proxy
__all__ = ('Proxy',)

View File

@@ -0,0 +1,17 @@
from typing import Optional, Tuple
import curio
import curio.io
import curio.socket
async def connect_tcp(
host: str,
port: int,
local_addr: Optional[Tuple[str, int]] = None,
) -> curio.io.Socket:
return await curio.open_connection(
host=host,
port=port,
source_addr=local_addr,
)

View File

@@ -0,0 +1,132 @@
from typing import Any, Optional
import warnings
import curio
import curio.io
from ..._types import ProxyType
from ..._helpers import parse_proxy_url
from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ._stream import CurioSocketStream
from ._resolver import Resolver
from ._connect import connect_tcp
from ..._protocols.errors import ReplyError
from ..._connectors.factory_async import create_connector
DEFAULT_TIMEOUT = 60
class CurioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._password = password
self._username = username
self._rdns = rdns
self._resolver = Resolver()
async def connect(
self,
dest_host: str,
dest_port: int,
timeout: Optional[float] = None,
**kwargs: Any,
) -> curio.io.Socket:
if timeout is None:
timeout = DEFAULT_TIMEOUT
_socket = kwargs.get('_socket')
if _socket is not None:
warnings.warn(
"The '_socket' argument is deprecated and will be removed in the future",
DeprecationWarning,
stacklevel=2,
)
local_addr = kwargs.get('local_addr')
try:
return await curio.timeout_after(
timeout,
self._connect,
dest_host,
dest_port,
_socket,
local_addr,
)
except curio.TaskTimeout as e:
raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e
async def _connect(
self,
dest_host: str,
dest_port: int,
_socket=None,
local_addr=None,
):
if _socket is None:
try:
_socket = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
local_addr=local_addr,
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
stream = CurioSocketStream(_socket)
try:
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
return _socket
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except BaseException:
await stream.close()
raise
@property
def proxy_host(self):
return self._proxy_host
@property
def proxy_port(self):
return self._proxy_port
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'CurioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,25 @@
import socket
from curio.socket import getaddrinfo
from ... import _abc as abc
class Resolver(abc.AsyncResolver):
async def resolve(self, host, port=0, family=socket.AF_UNSPEC):
try:
infos = await getaddrinfo(
host=host,
port=port,
family=family,
type=socket.SOCK_STREAM,
)
except socket.gaierror: # pragma: no cover
infos = None
if not infos: # pragma: no cover
raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family))
infos = sorted(infos, key=lambda info: info[0])
family, _, _, _, address = infos[0]
return family, address[0]

View File

@@ -0,0 +1,32 @@
import curio.io
import curio.socket
from ... import _abc as abc
from ..._errors import ProxyError
DEFAULT_RECEIVE_SIZE = 65536
class CurioSocketStream(abc.AsyncSocketStream):
_socket: curio.io.Socket = None
def __init__(self, sock: curio.io.Socket):
self._socket = sock
async def write_all(self, data):
await self._socket.sendall(data)
async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return await self._socket.recv(max_bytes)
async def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = await self._socket.recv(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def close(self):
await self._socket.close()

View File

@@ -0,0 +1,3 @@
from ._proxy import TrioProxy as Proxy
__all__ = ('Proxy',)

View File

@@ -0,0 +1,36 @@
from typing import Optional, Tuple
import trio
from ._resolver import Resolver
from ..._helpers import is_ipv4_address, is_ipv6_address
async def connect_tcp(
host: str,
port: int,
local_addr: Optional[Tuple[str, int]] = None,
) -> trio.socket.SocketType:
family, host = await _resolve_host(host)
sock = trio.socket.socket(family=family, type=trio.socket.SOCK_STREAM)
if local_addr is not None: # pragma: no cover
await sock.bind(local_addr)
try:
await sock.connect((host, port))
except OSError:
sock.close()
raise
return sock
async def _resolve_host(host):
if is_ipv4_address(host):
return trio.socket.AF_INET, host
if is_ipv6_address(host):
return trio.socket.AF_INET6, host
resolver = Resolver()
return await resolver.resolve(host=host)

View File

@@ -0,0 +1,131 @@
from typing import Any, Optional
import warnings
import trio
from ..._types import ProxyType
from ..._helpers import parse_proxy_url
from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ._stream import TrioSocketStream
from ._resolver import Resolver
from ._connect import connect_tcp
from ..._protocols.errors import ReplyError
from ..._connectors.factory_async import create_connector
DEFAULT_TIMEOUT = 60
class TrioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._password = password
self._username = username
self._rdns = rdns
self._resolver = Resolver()
async def connect(
self,
dest_host: str,
dest_port: int,
timeout: Optional[float] = None,
**kwargs: Any,
) -> trio.socket.SocketType:
if timeout is None:
timeout = DEFAULT_TIMEOUT
_socket = kwargs.get('_socket')
if _socket is not None:
warnings.warn(
"The '_socket' argument is deprecated and will be removed in the future",
DeprecationWarning,
stacklevel=2,
)
local_addr = kwargs.get('local_addr')
try:
with trio.fail_after(timeout):
return await self._connect(
dest_host=dest_host,
dest_port=dest_port,
_socket=_socket,
local_addr=local_addr,
)
except trio.TooSlowError as e:
raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e
async def _connect(
self,
dest_host: str,
dest_port: int,
_socket=None,
local_addr=None,
) -> trio.socket.SocketType:
if _socket is None:
try:
_socket = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
local_addr=local_addr,
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
stream = TrioSocketStream(sock=_socket)
try:
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
return _socket
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except BaseException: # trio.Cancelled...
with trio.CancelScope(shield=True):
await stream.close()
raise
@property
def proxy_host(self):
return self._proxy_host
@property
def proxy_port(self):
return self._proxy_port
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'TrioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,21 @@
import trio
from ... import _abc as abc
class Resolver(abc.AsyncResolver):
async def resolve(self, host, port=0, family=trio.socket.AF_UNSPEC):
infos = await trio.socket.getaddrinfo(
host=host,
port=port,
family=family,
type=trio.socket.SOCK_STREAM,
)
if not infos: # pragma: no cover
raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family))
infos = sorted(infos, key=lambda info: info[0])
family, _, _, _, address = infos[0]
return family, address[0]

View File

@@ -0,0 +1,35 @@
import trio
from ..._errors import ProxyError
from ... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
class TrioSocketStream(abc.AsyncSocketStream):
def __init__(self, sock):
self._socket = sock
async def write_all(self, data):
total_sent = 0
while total_sent < len(data):
remaining = data[total_sent:]
sent = await self._socket.send(remaining)
total_sent += sent
async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return await self._socket.recv(max_bytes)
async def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = await self._socket.recv(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def close(self):
if self._socket is not None:
self._socket.close()
await trio.lowlevel.checkpoint()

View File

@@ -0,0 +1,7 @@
from ._proxy import TrioProxy as Proxy
from ._chain import ProxyChain
__all__ = (
'Proxy',
'ProxyChain',
)

View File

@@ -0,0 +1,32 @@
from typing import Sequence
import warnings
from ._proxy import TrioProxy
class ProxyChain:
def __init__(self, proxies: Sequence[TrioProxy]):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
async def connect(
self,
dest_host,
dest_port,
dest_ssl=None,
timeout=None,
):
forward = None
for proxy in self._proxies:
proxy._forward = forward
forward = proxy
return await forward.connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
timeout=timeout,
)

View File

@@ -0,0 +1,17 @@
from typing import Optional
import trio
from ._stream import TrioSocketStream
async def connect_tcp(
host: str,
port: int,
local_addr: Optional[str] = None,
) -> TrioSocketStream:
trio_stream = await trio.open_tcp_stream(
host=host,
port=port,
local_address=local_addr,
)
return TrioSocketStream(trio_stream)

View File

@@ -0,0 +1,135 @@
import ssl
from typing import Any, Optional
import trio
from ._connect import connect_tcp
from ._stream import TrioSocketStream
from .._resolver import Resolver
from ...._types import ProxyType
from ...._helpers import parse_proxy_url
from ...._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ...._protocols.errors import ReplyError
from ...._connectors.factory_async import create_connector
DEFAULT_TIMEOUT = 60
class TrioProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
proxy_ssl: Optional[ssl.SSLContext] = None,
forward: Optional['TrioProxy'] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._forward = forward
self._resolver = Resolver()
async def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
timeout: Optional[float] = None,
**kwargs: Any,
) -> TrioSocketStream:
if timeout is None:
timeout = DEFAULT_TIMEOUT
local_addr = kwargs.get('local_addr')
try:
with trio.fail_after(timeout):
return await self._connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
local_addr=local_addr,
)
except trio.TooSlowError as e:
raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e
async def _connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
local_addr: Optional[str] = None,
) -> TrioSocketStream:
if self._forward is None:
try:
stream = await connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
local_addr=local_addr,
)
except OSError as e:
raise ProxyConnectionError(
e.errno,
"Couldn't connect to proxy"
f" {self._proxy_host}:{self._proxy_port} [{e.strerror}]",
) from e
else:
stream = await self._forward.connect(
dest_host=self._proxy_host,
dest_port=self._proxy_port,
)
try:
if self._proxy_ssl is not None:
stream = await stream.start_tls(
hostname=self._proxy_host,
ssl_context=self._proxy_ssl,
)
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
await connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
if dest_ssl is not None:
stream = await stream.start_tls(
hostname=dest_host,
ssl_context=dest_ssl,
)
except ReplyError as e:
await stream.close()
raise ProxyError(e, error_code=e.error_code)
except BaseException: # trio.Cancelled...
with trio.CancelScope(shield=True):
await stream.close()
raise
return stream
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'TrioProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,55 @@
import ssl
from typing import Union
import trio
from ...._errors import ProxyError
from .... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
TrioStreamType = Union[trio.SocketStream, trio.SSLStream]
class TrioSocketStream(abc.AsyncSocketStream):
_stream: TrioStreamType
def __init__(self, stream: TrioStreamType):
self._stream = stream
async def write_all(self, data):
await self._stream.send_all(data)
async def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return await self._stream.receive_some(max_bytes)
async def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = await self._stream.receive_some(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
async def start_tls(
self,
hostname: str,
ssl_context: ssl.SSLContext,
) -> 'TrioSocketStream':
ssl_stream = trio.SSLStream(
self._stream,
ssl_context=ssl_context,
server_hostname=hostname,
https_compatible=True,
server_side=False,
)
await ssl_stream.do_handshake()
return TrioSocketStream(ssl_stream)
async def close(self):
await self._stream.aclose()
@property
def trio_stream(self) -> TrioStreamType: # pragma: nocover
return self._stream

View File

@@ -0,0 +1,5 @@
from ._proxy import SyncProxy as Proxy
from ._chain import ProxyChain
__all__ = ('Proxy', 'ProxyChain')

View File

@@ -0,0 +1,32 @@
from typing import Iterable
import warnings
from ._proxy import SyncProxy
class ProxyChain:
def __init__(self, proxies: Iterable[SyncProxy]):
warnings.warn(
'This implementation of ProxyChain is deprecated and will be removed in the future',
DeprecationWarning,
stacklevel=2,
)
self._proxies = proxies
def connect(self, dest_host, dest_port, timeout=None):
curr_socket = None
proxies = list(self._proxies)
length = len(proxies) - 1
for i in range(length):
curr_socket = proxies[i].connect(
dest_host=proxies[i + 1].proxy_host,
dest_port=proxies[i + 1].proxy_port,
timeout=timeout,
_socket=curr_socket,
)
curr_socket = proxies[length].connect(
dest_host=dest_host, dest_port=dest_port, timeout=timeout, _socket=curr_socket
)
return curr_socket

View File

@@ -0,0 +1,16 @@
import socket
from typing import Optional, Tuple
def connect_tcp(
host: str,
port: int,
timeout: Optional[float] = None,
local_addr: Optional[Tuple[str, int]] = None,
) -> socket.socket:
address = (host, port)
return socket.create_connection(
address,
timeout,
source_address=local_addr,
)

View File

@@ -0,0 +1,116 @@
import socket
from typing import Optional, Any
import warnings
from .._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from .._types import ProxyType
from .._helpers import parse_proxy_url
from .._protocols.errors import ReplyError
from .._connectors.factory_sync import create_connector
from ._stream import SyncSocketStream
from ._resolver import SyncResolver
from ._connect import connect_tcp
DEFAULT_TIMEOUT = 60
class SyncProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._password = password
self._username = username
self._rdns = rdns
self._resolver = SyncResolver()
def connect(
self,
dest_host: str,
dest_port: int,
timeout: Optional[float] = None,
**kwargs: Any,
) -> socket.socket:
if timeout is None:
timeout = DEFAULT_TIMEOUT
_socket = kwargs.get('_socket')
if _socket is not None:
warnings.warn(
"The '_socket' argument is deprecated and will be removed in the future",
DeprecationWarning,
stacklevel=2,
)
if _socket is None:
local_addr = kwargs.get('local_addr')
try:
_socket = connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
timeout=timeout,
local_addr=local_addr,
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
stream = SyncSocketStream(_socket)
try:
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
return _socket
except socket.timeout as e:
stream.close()
raise ProxyTimeoutError('Proxy connection timed out: {}'.format(timeout)) from e
except ReplyError as e:
stream.close()
raise ProxyError(e, error_code=e.error_code)
except Exception:
stream.close()
raise
@property
def proxy_host(self):
return self._proxy_host
@property
def proxy_port(self):
return self._proxy_port
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'SyncProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,16 @@
import socket
from .. import _abc as abc
class SyncResolver(abc.SyncResolver):
# noinspection PyMethodMayBeStatic
def resolve(self, host, port=0, family=socket.AF_UNSPEC):
infos = socket.getaddrinfo(host=host, port=port, family=family, type=socket.SOCK_STREAM)
if not infos: # pragma: no cover
raise OSError('Can`t resolve address {}:{} [{}]'.format(host, port, family))
infos = sorted(infos, key=lambda info: info[0])
family, _, _, _, address = infos[0]
return family, address[0]

View File

@@ -0,0 +1,32 @@
import socket
from .._errors import ProxyError
from .. import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
class SyncSocketStream(abc.SyncSocketStream):
_socket: socket.socket
def __init__(self, sock: socket.socket):
self._socket = sock
def write_all(self, data):
self._socket.sendall(data)
def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return self._socket.recv(max_bytes)
def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = self._socket.recv(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
def close(self):
if self._socket is not None:
self._socket.close()

View File

@@ -0,0 +1,7 @@
from ._proxy import SyncProxy as Proxy
from ._chain import ProxyChain
__all__ = (
'Proxy',
'ProxyChain',
)

View File

@@ -0,0 +1,26 @@
from typing import Iterable
from ._proxy import SyncProxy
class ProxyChain:
def __init__(self, proxies: Iterable[SyncProxy]):
self._proxies = proxies
def connect(
self,
dest_host,
dest_port,
dest_ssl=None,
timeout=None,
):
forward = None
for proxy in self._proxies:
proxy._forward = forward
forward = proxy
return forward.connect(
dest_host=dest_host,
dest_port=dest_port,
dest_ssl=dest_ssl,
timeout=timeout,
)

View File

@@ -0,0 +1,19 @@
import socket
from typing import Optional, Tuple
from ._stream import SyncSocketStream
def connect_tcp(
host: str,
port: int,
timeout: Optional[float] = None,
local_addr: Optional[Tuple[str, int]] = None,
) -> SyncSocketStream:
address = (host, port)
sock = socket.create_connection(
address,
timeout,
source_address=local_addr,
)
return SyncSocketStream(sock)

View File

@@ -0,0 +1,121 @@
import socket
import ssl
from typing import Any, Optional
from ._connect import connect_tcp
from ._stream import SyncSocketStream
from .._resolver import SyncResolver
from ..._types import ProxyType
from ..._errors import ProxyConnectionError, ProxyTimeoutError, ProxyError
from ..._helpers import parse_proxy_url
from ..._protocols.errors import ReplyError
from ..._connectors.factory_sync import create_connector
DEFAULT_TIMEOUT = 60
class SyncProxy:
def __init__(
self,
proxy_type: ProxyType,
host: str,
port: int,
username: Optional[str] = None,
password: Optional[str] = None,
rdns: Optional[bool] = None,
proxy_ssl: Optional[ssl.SSLContext] = None,
forward: Optional['SyncProxy'] = None,
):
self._proxy_type = proxy_type
self._proxy_host = host
self._proxy_port = port
self._username = username
self._password = password
self._rdns = rdns
self._proxy_ssl = proxy_ssl
self._forward = forward
self._resolver = SyncResolver()
def connect(
self,
dest_host: str,
dest_port: int,
dest_ssl: Optional[ssl.SSLContext] = None,
timeout: Optional[float] = None,
**kwargs: Any,
) -> SyncSocketStream:
if timeout is None:
timeout = DEFAULT_TIMEOUT
if self._forward is None:
local_addr = kwargs.get('local_addr')
try:
stream = connect_tcp(
host=self._proxy_host,
port=self._proxy_port,
timeout=timeout,
local_addr=local_addr,
)
except OSError as e:
msg = 'Could not connect to proxy {}:{} [{}]'.format(
self._proxy_host,
self._proxy_port,
e.strerror,
)
raise ProxyConnectionError(e.errno, msg) from e
else:
stream = self._forward.connect(
dest_host=self._proxy_host,
dest_port=self._proxy_port,
timeout=timeout,
)
try:
if self._proxy_ssl is not None:
stream = stream.start_tls(
hostname=self._proxy_host,
ssl_context=self._proxy_ssl,
)
connector = create_connector(
proxy_type=self._proxy_type,
username=self._username,
password=self._password,
rdns=self._rdns,
resolver=self._resolver,
)
connector.connect(
stream=stream,
host=dest_host,
port=dest_port,
)
if dest_ssl is not None:
stream = stream.start_tls(
hostname=dest_host,
ssl_context=dest_ssl,
)
return stream
except socket.timeout as e:
stream.close()
raise ProxyTimeoutError(f'Proxy connection timed out: {timeout}') from e
except ReplyError as e:
stream.close()
raise ProxyError(e, error_code=e.error_code)
except Exception:
stream.close()
raise
@classmethod
def create(cls, *args, **kwargs): # for backward compatibility
return cls(*args, **kwargs)
@classmethod
def from_url(cls, url: str, **kwargs) -> 'SyncProxy':
url_args = parse_proxy_url(url)
return cls(*url_args, **kwargs)

View File

@@ -0,0 +1,200 @@
"""
Copied from urllib3.util.ssltransport
"""
import io
import socket
import ssl
SSL_BLOCKSIZE = 16384
class SSLTransport:
"""
The SSLTransport wraps an existing socket and establishes an SSL connection.
Contrary to Python's implementation of SSLSocket, it allows you to chain
multiple TLS connections together. It's particularly useful if you need to
implement TLS within TLS.
The class supports most of the socket API operations.
"""
def __init__(
self, socket, ssl_context, server_hostname=None, suppress_ragged_eofs=True
):
"""
Create an SSLTransport around socket using the provided ssl_context.
"""
self.incoming = ssl.MemoryBIO()
self.outgoing = ssl.MemoryBIO()
self.suppress_ragged_eofs = suppress_ragged_eofs
self.socket = socket
self.sslobj = ssl_context.wrap_bio(
self.incoming, self.outgoing, server_hostname=server_hostname
)
# Perform initial handshake.
self._ssl_io_loop(self.sslobj.do_handshake)
def __enter__(self):
return self
def __exit__(self, *_):
self.close()
def fileno(self):
return self.socket.fileno()
def read(self, len=1024, buffer=None):
return self._wrap_ssl_read(len, buffer)
def recv(self, len=1024, flags=0):
if flags != 0:
raise ValueError("non-zero flags not allowed in calls to recv")
return self._wrap_ssl_read(len)
def recv_into(self, buffer, nbytes=None, flags=0):
if flags != 0:
raise ValueError("non-zero flags not allowed in calls to recv_into")
if buffer and (nbytes is None):
nbytes = len(buffer)
elif nbytes is None:
nbytes = 1024
return self.read(nbytes, buffer)
def sendall(self, data, flags=0):
if flags != 0:
raise ValueError("non-zero flags not allowed in calls to sendall")
count = 0
with memoryview(data) as view, view.cast("B") as byte_view:
amount = len(byte_view)
while count < amount:
v = self.send(byte_view[count:])
count += v
def send(self, data, flags=0):
if flags != 0:
raise ValueError("non-zero flags not allowed in calls to send")
response = self._ssl_io_loop(self.sslobj.write, data)
return response
def makefile(
self, mode="r", buffering=None, encoding=None, errors=None, newline=None
):
"""
Python's httpclient uses makefile and buffered io when reading HTTP
messages and we need to support it.
This is unfortunately a copy and paste of socket.py makefile with small
changes to point to the socket directly.
"""
if not set(mode) <= {"r", "w", "b"}:
raise ValueError("invalid mode %r (only r, w, b allowed)" % (mode,))
writing = "w" in mode
reading = "r" in mode or not writing
assert reading or writing
binary = "b" in mode
rawmode = ""
if reading:
rawmode += "r"
if writing:
rawmode += "w"
raw = socket.SocketIO(self, rawmode)
self.socket._io_refs += 1
if buffering is None:
buffering = -1
if buffering < 0:
buffering = io.DEFAULT_BUFFER_SIZE
if buffering == 0:
if not binary:
raise ValueError("unbuffered streams must be binary")
return raw
if reading and writing:
buffer = io.BufferedRWPair(raw, raw, buffering)
elif reading:
buffer = io.BufferedReader(raw, buffering)
else:
assert writing
buffer = io.BufferedWriter(raw, buffering)
if binary:
return buffer
text = io.TextIOWrapper(buffer, encoding, errors, newline)
text.mode = mode
return text
def unwrap(self):
self._ssl_io_loop(self.sslobj.unwrap)
def close(self):
self.socket.close()
def getpeercert(self, binary_form=False):
return self.sslobj.getpeercert(binary_form)
def version(self):
return self.sslobj.version()
def cipher(self):
return self.sslobj.cipher()
def selected_alpn_protocol(self):
return self.sslobj.selected_alpn_protocol()
def selected_npn_protocol(self):
return self.sslobj.selected_npn_protocol()
def shared_ciphers(self):
return self.sslobj.shared_ciphers()
def compression(self):
return self.sslobj.compression()
def settimeout(self, value):
self.socket.settimeout(value)
def gettimeout(self):
return self.socket.gettimeout()
def _decref_socketios(self):
self.socket._decref_socketios()
def _wrap_ssl_read(self, len, buffer=None):
try:
return self._ssl_io_loop(self.sslobj.read, len, buffer)
except ssl.SSLError as e:
if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs:
return 0 # eof, return 0.
else:
raise
def _ssl_io_loop(self, func, *args):
"""Performs an I/O loop between incoming/outgoing and the socket."""
should_loop = True
ret = None
while should_loop:
errno = None
try:
ret = func(*args)
except ssl.SSLError as e:
if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE):
# WANT_READ, and WANT_WRITE are expected, others are not.
raise e
errno = e.errno
buf = self.outgoing.read()
self.socket.sendall(buf)
if errno is None:
should_loop = False
elif errno == ssl.SSL_ERROR_WANT_READ:
buf = self.socket.recv(SSL_BLOCKSIZE)
if buf:
self.incoming.write(buf)
else:
self.incoming.write_eof()
return ret

View File

@@ -0,0 +1,56 @@
import socket
import ssl
from typing import Union
from ._ssl_transport import SSLTransport
from ..._errors import ProxyError
from ... import _abc as abc
DEFAULT_RECEIVE_SIZE = 65536
SocketType = Union[socket.socket, ssl.SSLSocket, SSLTransport]
class SyncSocketStream(abc.SyncSocketStream):
_socket: SocketType
def __init__(self, sock: SocketType):
self._socket = sock
def write_all(self, data):
self._socket.sendall(data)
def read(self, max_bytes=DEFAULT_RECEIVE_SIZE):
return self._socket.recv(max_bytes)
def read_exact(self, n):
data = bytearray()
while len(data) < n:
packet = self._socket.recv(n - len(data))
if not packet: # pragma: no cover
raise ProxyError('Connection closed unexpectedly')
data += packet
return data
def start_tls(self, hostname: str, ssl_context: ssl.SSLContext) -> 'SyncSocketStream':
if isinstance(self._socket, (ssl.SSLSocket, SSLTransport)):
ssl_socket = SSLTransport(
self._socket,
ssl_context=ssl_context,
server_hostname=hostname,
)
else: # plain socket?
ssl_socket = ssl_context.wrap_socket(
self._socket,
server_hostname=hostname,
)
return SyncSocketStream(ssl_socket)
def close(self):
self._socket.close()
@property
def socket(self) -> SocketType: # pragma: nocover
return self._socket

View File

@@ -17,11 +17,14 @@ WORKDIR /app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai
# Alpine 使用国内镜像,加速 apk 安装python3/py3-pip # Alpine 使用国内镜像,加速 apk 安装python3/py3-pip并设置时区为 Asia/Shanghai与宿主机保持一致
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk add --no-cache python3 py3-pip && apk add --no-cache python3 py3-pip tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone
COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist COPY --from=build /app/dist ./dist
@@ -49,7 +52,9 @@ RUN pip3 install --no-cache-dir --break-system-packages \
python3 -c "import fastapi; import uvicorn; import httpx; import websockets; import openai; print('all deps ok')" python3 -c "import fastapi; import uvicorn; import httpx; import websockets; import openai; print('all deps ok')"
COPY backend ./backend COPY backend ./backend
# 仅复制模板,不复制 .env / .env.prod由 run-docker.sh --env-file .env.prod 注入)
COPY .env.example ./ COPY .env.example ./
COPY .env.prod.example ./
COPY start.sh ./ COPY start.sh ./
EXPOSE 3000 8000 EXPOSE 3000 8000

View File

@@ -3,11 +3,16 @@ FROM docker.m.daocloud.io/library/python:3.11-slim
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1 \
TZ=Asia/Shanghai
COPY backend/requirements.txt . COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN apt-get update && apt-get install -y --no-install-recommends tzdata \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY backend ./backend COPY backend ./backend

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More