mirror of
https://github.com/violettoolssite/CFspider.git
synced 2026-04-05 11:29:03 +08:00
v1.8.4: Add two-proxy support for custom exit IP region
This commit is contained in:
@@ -73,6 +73,13 @@ from .batch import batch, abatch, BatchResult, BatchItem
|
||||
# 数据导出
|
||||
from .export import export
|
||||
|
||||
# 本地代理服务器(支持双层代理)
|
||||
from .proxy_server import (
|
||||
generate_vless_link,
|
||||
start_proxy_server,
|
||||
TwoProxyServer
|
||||
)
|
||||
|
||||
# 异步 API(基于 httpx)
|
||||
from .async_api import (
|
||||
aget, apost, aput, adelete, ahead, aoptions, apatch,
|
||||
@@ -210,7 +217,7 @@ class PlaywrightNotInstalledError(CFSpiderError):
|
||||
pass
|
||||
|
||||
|
||||
__version__ = "1.8.3"
|
||||
__version__ = "1.8.4"
|
||||
__all__ = [
|
||||
# 同步 API (requests)
|
||||
"get", "post", "put", "delete", "head", "options", "patch", "request",
|
||||
@@ -242,4 +249,6 @@ __all__ = [
|
||||
"batch", "abatch", "BatchResult", "BatchItem",
|
||||
# 数据导出
|
||||
"export",
|
||||
# 本地代理服务器(双层代理)
|
||||
"generate_vless_link", "start_proxy_server", "TwoProxyServer",
|
||||
]
|
||||
|
||||
104
cfspider/api.py
104
cfspider/api.py
@@ -382,7 +382,7 @@ class CFSpiderResponse:
|
||||
def request(method, url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""
|
||||
发送 HTTP 请求
|
||||
|
||||
@@ -404,6 +404,12 @@ def request(method, url, cf_proxies=None, uuid=None, http2=False, impersonate=No
|
||||
- False: 每次请求获取新的出口 IP(适合大规模采集)
|
||||
- True: 保持使用同一个 IP(适合需要会话一致性的场景)
|
||||
|
||||
two_proxy: 第二层代理(可选)
|
||||
格式: "host:port:user:pass" 或 "host:port"
|
||||
例如: "us.cliproxy.io:3010:username:password"
|
||||
用于无法直连第二层代理时,通过 Workers 作为跳板
|
||||
流程: 本地 → Workers (VLESS) → 第二层代理 → 目标网站
|
||||
|
||||
http2: 是否启用 HTTP/2 协议(默认 False)
|
||||
|
||||
impersonate: TLS 指纹模拟(可选)
|
||||
@@ -471,7 +477,7 @@ def request(method, url, cf_proxies=None, uuid=None, http2=False, impersonate=No
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser,
|
||||
static_ip=static_ip,
|
||||
static_ip=static_ip, two_proxy=two_proxy,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@@ -652,7 +658,7 @@ def _request_vless(method, url, cf_proxies, uuid=None,
|
||||
http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome',
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""
|
||||
使用 VLESS 协议发送请求
|
||||
|
||||
@@ -667,27 +673,33 @@ def _request_vless(method, url, cf_proxies, uuid=None,
|
||||
static_ip: 是否使用固定 IP(默认 False)
|
||||
- False: 每次请求获取新的出口 IP(适合大规模采集)
|
||||
- True: 保持使用同一个 IP(适合需要会话一致性的场景)
|
||||
two_proxy: 第二层代理(可选)
|
||||
格式: "host:port:user:pass"
|
||||
流程: 本地 → Workers (VLESS) → 第二层代理 → 目标网站
|
||||
其他参数与 request() 相同
|
||||
"""
|
||||
from .vless_client import LocalVlessProxy
|
||||
import uuid as uuid_mod
|
||||
|
||||
# 获取 Workers 配置
|
||||
# 解析 Workers 地址获取 host
|
||||
parsed = urlparse(cf_proxies)
|
||||
if parsed.scheme:
|
||||
host = parsed.netloc or parsed.path.split('/')[0]
|
||||
else:
|
||||
host = cf_proxies.split('/')[0]
|
||||
|
||||
# 尝试从 Workers 获取配置
|
||||
config = _get_workers_config(cf_proxies)
|
||||
if not config:
|
||||
raise ValueError(
|
||||
f"无法从 {cf_proxies} 获取配置。\n"
|
||||
"请确保 Workers 已正确部署。"
|
||||
)
|
||||
|
||||
# 解析配置
|
||||
host = config.get('host')
|
||||
is_default_uuid = config.get('is_default_uuid', True)
|
||||
|
||||
# 如果用户提供了 uuid,使用用户的
|
||||
# 如果用户没提供,尝试从 Workers 获取(仅默认 UUID 可获取)
|
||||
# 如果用户没提供 uuid,使用 Workers 配置的 uuid
|
||||
if not uuid:
|
||||
if not config:
|
||||
raise ValueError(
|
||||
f"无法从 {cf_proxies} 获取配置。\n"
|
||||
"请确保 Workers 已正确部署,或手动指定 uuid 参数。"
|
||||
)
|
||||
uuid = config.get('uuid')
|
||||
host = config.get('host', host)
|
||||
|
||||
if not uuid:
|
||||
# Workers 配置了自定义 UUID,需要用户手动填写
|
||||
@@ -697,23 +709,42 @@ def _request_vless(method, url, cf_proxies, uuid=None,
|
||||
"提示: UUID 可在 Workers 界面或 Cloudflare Dashboard 中查看。"
|
||||
)
|
||||
|
||||
# 自动获取 Workers 配置的 two_proxy
|
||||
# 优先级:用户传入的 two_proxy > Workers 配置的 two_proxy
|
||||
if two_proxy is None and config:
|
||||
# 检查 Workers 是否配置了双层代理
|
||||
if config.get('two_proxy_enabled'):
|
||||
# 优先使用完整的 two_proxy 配置
|
||||
workers_two_proxy = config.get('two_proxy')
|
||||
if workers_two_proxy:
|
||||
two_proxy = workers_two_proxy
|
||||
else:
|
||||
# 尝试从 vless_path 中解析
|
||||
vless_path = config.get('vless_path', '')
|
||||
if 'two_proxy=' in vless_path:
|
||||
import re
|
||||
match = re.search(r'two_proxy=([^&]+)', vless_path)
|
||||
if match:
|
||||
from urllib.parse import unquote
|
||||
two_proxy = unquote(match.group(1))
|
||||
|
||||
# 构建 VLESS WebSocket URL: wss://host/uuid
|
||||
vless_url = f"wss://{host}/{uuid}"
|
||||
|
||||
# 根据 static_ip 参数决定是否复用连接
|
||||
if static_ip:
|
||||
# 固定 IP 模式:复用同一个连接
|
||||
cache_key = f"{host}:{uuid}"
|
||||
cache_key = f"{host}:{uuid}:{two_proxy or ''}"
|
||||
if cache_key in _vless_proxy_cache:
|
||||
proxy = _vless_proxy_cache[cache_key]
|
||||
port = proxy.port
|
||||
else:
|
||||
proxy = LocalVlessProxy(vless_url, uuid)
|
||||
proxy = LocalVlessProxy(vless_url, uuid, two_proxy=two_proxy)
|
||||
port = proxy.start()
|
||||
_vless_proxy_cache[cache_key] = proxy
|
||||
else:
|
||||
# 动态 IP 模式:每次创建新连接
|
||||
proxy = LocalVlessProxy(vless_url, uuid)
|
||||
proxy = LocalVlessProxy(vless_url, uuid, two_proxy=two_proxy)
|
||||
port = proxy.start()
|
||||
|
||||
# 构建本地代理 URL
|
||||
@@ -827,7 +858,7 @@ def stop_vless_proxies():
|
||||
def get(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""
|
||||
发送 GET 请求 / Send GET request
|
||||
|
||||
@@ -845,6 +876,11 @@ def get(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
- False: 每次请求获取新的出口 IP(适合大规模采集)
|
||||
- True: 保持使用同一个 IP(适合需要会话一致性的场景)
|
||||
|
||||
two_proxy: 第二层代理(可选)
|
||||
格式: "host:port:user:pass" 或 "host:port"
|
||||
例如: "us.cliproxy.io:3010:username:password"
|
||||
流程: 本地 → Workers (VLESS) → 第二层代理 → 目标网站
|
||||
|
||||
http2: 是否启用 HTTP/2 协议(默认 False)
|
||||
|
||||
impersonate: TLS 指纹模拟(可选)
|
||||
@@ -872,90 +908,90 @@ def get(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
... cf_proxies="https://cfspider.violetqqcom.workers.dev"
|
||||
... )
|
||||
>>>
|
||||
>>> # 固定 IP(保持同一个 IP)
|
||||
>>> # 使用第二层代理(通过 Workers 连接到日本代理)
|
||||
>>> response = cfspider.get(
|
||||
... "https://httpbin.org/ip",
|
||||
... cf_proxies="https://cfspider.violetqqcom.workers.dev",
|
||||
... static_ip=True
|
||||
... two_proxy="us.cliproxy.io:3010:username:password"
|
||||
... )
|
||||
"""
|
||||
return request("GET", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def post(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 POST 请求 / Send POST request"""
|
||||
return request("POST", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def put(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 PUT 请求 / Send PUT request"""
|
||||
return request("PUT", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def delete(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 DELETE 请求 / Send DELETE request"""
|
||||
return request("DELETE", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def head(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 HEAD 请求 / Send HEAD request"""
|
||||
return request("HEAD", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def options(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 OPTIONS 请求 / Send OPTIONS request"""
|
||||
return request("OPTIONS", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def patch(url, cf_proxies=None, uuid=None, http2=False, impersonate=None,
|
||||
map_output=False, map_file="cfspider_map.html",
|
||||
stealth=False, stealth_browser='chrome', delay=None,
|
||||
static_ip=False, **kwargs):
|
||||
static_ip=False, two_proxy=None, **kwargs):
|
||||
"""发送 PATCH 请求 / Send PATCH request"""
|
||||
return request("PATCH", url, cf_proxies=cf_proxies, uuid=uuid,
|
||||
http2=http2, impersonate=impersonate,
|
||||
map_output=map_output, map_file=map_file,
|
||||
stealth=stealth, stealth_browser=stealth_browser, delay=delay,
|
||||
static_ip=static_ip, **kwargs)
|
||||
static_ip=static_ip, two_proxy=two_proxy, **kwargs)
|
||||
|
||||
|
||||
def clear_map_records():
|
||||
|
||||
@@ -117,8 +117,8 @@ class Browser:
|
||||
- CFspider Workers URL(推荐): "https://cfspider.violetqqcom.workers.dev"
|
||||
UUID 将自动从 Workers 获取 / UUID auto-fetched from Workers
|
||||
- VLESS 链接: "vless://uuid@host:port?path=/xxx#name"
|
||||
- HTTP 代理: "http://ip:port" 或 "ip:port"
|
||||
- SOCKS5 代理: "socks5://ip:port"
|
||||
- HTTP 代理: "http://ip:port" 或 "ip:port"
|
||||
- SOCKS5 代理: "socks5://ip:port"
|
||||
不填则直接使用本地网络 / None for direct connection
|
||||
headless (bool): 是否无头模式,默认 True / Headless mode (default: True)
|
||||
timeout (int): 请求超时时间(秒),默认 30 / Timeout in seconds (default: 30)
|
||||
@@ -176,7 +176,7 @@ class Browser:
|
||||
proxy_url = f"http://127.0.0.1:{port}"
|
||||
else:
|
||||
# 直接使用 HTTP 代理
|
||||
proxy_url = cf_proxies
|
||||
proxy_url = cf_proxies
|
||||
# 3. IP:PORT 格式
|
||||
elif ':' in cf_proxies and cf_proxies.replace('.', '').replace(':', '').isdigit():
|
||||
proxy_url = f"http://{cf_proxies}"
|
||||
@@ -187,10 +187,10 @@ class Browser:
|
||||
if uuid:
|
||||
ws_url = f'wss://{hostname}/{uuid}'
|
||||
self._vless_proxy = LocalVlessProxy(ws_url, uuid)
|
||||
port = self._vless_proxy.start()
|
||||
proxy_url = f"http://127.0.0.1:{port}"
|
||||
else:
|
||||
proxy_url = f"http://{cf_proxies}"
|
||||
port = self._vless_proxy.start()
|
||||
proxy_url = f"http://127.0.0.1:{port}"
|
||||
else:
|
||||
proxy_url = f"http://{cf_proxies}"
|
||||
|
||||
def _get_workers_uuid(self, workers_url):
|
||||
"""从 Workers 获取 UUID / Get UUID from Workers"""
|
||||
|
||||
350
cfspider/proxy_server.py
Normal file
350
cfspider/proxy_server.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CFspider 本地代理服务器
|
||||
支持双层代理(two_proxy),可供 v2ray 或浏览器使用
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from urllib.parse import urlparse, quote
|
||||
|
||||
|
||||
def generate_vless_link(
|
||||
cf_proxies: str,
|
||||
uuid: str,
|
||||
name: str = "CFspider",
|
||||
two_proxy: str = None
|
||||
) -> dict:
|
||||
"""
|
||||
生成 VLESS 导入链接
|
||||
|
||||
Args:
|
||||
cf_proxies: Workers 地址,如 "https://your-workers.dev"
|
||||
uuid: VLESS UUID
|
||||
name: 节点名称
|
||||
two_proxy: 第二层代理(可选),格式 "host:port:user:pass"
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'vless_link': str, # VLESS 链接(可导入 v2ray)
|
||||
'local_proxy_cmd': str, # 启动本地代理的命令
|
||||
'note': str # 使用说明
|
||||
}
|
||||
|
||||
Example:
|
||||
>>> result = cfspider.generate_vless_link(
|
||||
... cf_proxies="https://your-workers.dev",
|
||||
... uuid="your-uuid",
|
||||
... two_proxy="us.cliproxy.io:3010:user:pass"
|
||||
... )
|
||||
>>> print(result['vless_link'])
|
||||
"""
|
||||
# 解析 Workers 地址
|
||||
parsed = urlparse(cf_proxies)
|
||||
if parsed.scheme:
|
||||
host = parsed.netloc or parsed.path.split('/')[0]
|
||||
else:
|
||||
host = cf_proxies.split('/')[0]
|
||||
|
||||
# 生成基础 VLESS 链接(连接到 Workers)
|
||||
vless_params = {
|
||||
'security': 'tls',
|
||||
'type': 'ws',
|
||||
'host': host,
|
||||
'sni': host,
|
||||
'path': f'/{uuid}',
|
||||
'encryption': 'none'
|
||||
}
|
||||
|
||||
params_str = '&'.join([f'{k}={quote(str(v))}' for k, v in vless_params.items()])
|
||||
vless_link = f"vless://{uuid}@{host}:443?{params_str}#{quote(name)}"
|
||||
|
||||
result = {
|
||||
'vless_link': vless_link,
|
||||
'host': host,
|
||||
'uuid': uuid,
|
||||
}
|
||||
|
||||
if two_proxy:
|
||||
# 解析第二层代理
|
||||
parts = two_proxy.split(':')
|
||||
if len(parts) >= 2:
|
||||
proxy_host = parts[0]
|
||||
proxy_port = parts[1]
|
||||
proxy_info = f"{proxy_host}:{proxy_port}"
|
||||
if len(parts) == 4:
|
||||
proxy_info += f" (user: {parts[2][:4]}***)"
|
||||
else:
|
||||
proxy_info = two_proxy
|
||||
|
||||
# 生成本地代理启动命令
|
||||
result['local_proxy_cmd'] = (
|
||||
f'python -m cfspider.proxy_server '
|
||||
f'--cf-proxies "{cf_proxies}" '
|
||||
f'--uuid "{uuid}" '
|
||||
f'--two-proxy "{two_proxy}" '
|
||||
f'--port 1080'
|
||||
)
|
||||
|
||||
result['note'] = f"""
|
||||
=== 双层代理使用说明 ===
|
||||
|
||||
由于 VLESS 协议本身不支持代理链,有两种方式使用双层代理:
|
||||
|
||||
【方式 1】使用 CFspider 本地代理(推荐)
|
||||
运行以下命令启动本地代理服务器:
|
||||
|
||||
{result['local_proxy_cmd']}
|
||||
|
||||
然后在浏览器或系统中配置代理为:
|
||||
HTTP/SOCKS5 代理: 127.0.0.1:1080
|
||||
|
||||
流量链路: 浏览器 → 本地代理(1080) → Workers(VLESS) → {proxy_info} → 目标网站
|
||||
|
||||
【方式 2】在 Workers 端配置 PROXYIP
|
||||
1. 打开 Cloudflare Dashboard → Workers
|
||||
2. 选择你的 Worker → Settings → Variables
|
||||
3. 添加环境变量: PROXYIP = {parts[0] if len(parts) >= 2 else two_proxy}
|
||||
4. 然后使用以下 VLESS 链接导入 v2ray:
|
||||
|
||||
{vless_link}
|
||||
|
||||
注意:方式 2 需要第二层代理支持无认证连接,或在 Workers 代码中配置认证。
|
||||
"""
|
||||
else:
|
||||
result['note'] = f"""
|
||||
=== VLESS 链接使用说明 ===
|
||||
|
||||
将以下链接导入 v2ray/clash 等客户端:
|
||||
|
||||
{vless_link}
|
||||
|
||||
流量链路: v2ray 客户端 → Workers(VLESS) → 目标网站
|
||||
出口 IP: Cloudflare WARP 网络 IP
|
||||
"""
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class TwoProxyServer:
|
||||
"""
|
||||
双层代理本地服务器
|
||||
|
||||
提供 HTTP/SOCKS5 代理接口,内部通过 VLESS + two_proxy 实现
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cf_proxies: str,
|
||||
uuid: str,
|
||||
two_proxy: str = None,
|
||||
host: str = '127.0.0.1',
|
||||
port: int = 1080
|
||||
):
|
||||
"""
|
||||
初始化本地代理服务器
|
||||
|
||||
Args:
|
||||
cf_proxies: Workers 地址
|
||||
uuid: VLESS UUID
|
||||
two_proxy: 第二层代理,格式 "host:port:user:pass"
|
||||
host: 监听地址(默认 127.0.0.1)
|
||||
port: 监听端口(默认 1080)
|
||||
"""
|
||||
self.cf_proxies = cf_proxies
|
||||
self.uuid = uuid
|
||||
self.two_proxy = two_proxy
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server = None
|
||||
self.running = False
|
||||
self._vless_proxy = None
|
||||
self._vless_port = None
|
||||
|
||||
def _ensure_vless_proxy(self):
|
||||
"""确保 VLESS 代理已启动"""
|
||||
if self._vless_proxy is None:
|
||||
from .vless_client import LocalVlessProxy
|
||||
|
||||
# 解析 Workers 地址
|
||||
parsed = urlparse(self.cf_proxies)
|
||||
if parsed.scheme:
|
||||
host = parsed.netloc or parsed.path.split('/')[0]
|
||||
else:
|
||||
host = self.cf_proxies.split('/')[0]
|
||||
|
||||
vless_url = f"wss://{host}/{self.uuid}"
|
||||
print(f"[INIT] Starting VLESS proxy: {vless_url}")
|
||||
if self.two_proxy:
|
||||
print(f"[INIT] Two-proxy enabled: {self.two_proxy.split(':')[0]}:{self.two_proxy.split(':')[1]}")
|
||||
|
||||
self._vless_proxy = LocalVlessProxy(vless_url, self.uuid, two_proxy=self.two_proxy)
|
||||
self._vless_port = self._vless_proxy.start()
|
||||
print(f"[INIT] VLESS proxy started on port {self._vless_port}")
|
||||
|
||||
def start(self):
|
||||
"""启动代理服务器"""
|
||||
self._ensure_vless_proxy()
|
||||
|
||||
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.server.bind((self.host, self.port))
|
||||
self.server.listen(50)
|
||||
|
||||
self.running = True
|
||||
|
||||
print(f"=" * 60)
|
||||
print(f"CFspider Two-Proxy Server Started")
|
||||
print(f"=" * 60)
|
||||
print(f"Local Proxy: {self.host}:{self.port}")
|
||||
print(f"Workers: {self.cf_proxies}")
|
||||
if self.two_proxy:
|
||||
parts = self.two_proxy.split(':')
|
||||
print(f"Second Proxy: {parts[0]}:{parts[1]}")
|
||||
print(f"-" * 60)
|
||||
print(f"Configure your browser/system proxy to: {self.host}:{self.port}")
|
||||
print(f"Press Ctrl+C to stop")
|
||||
print(f"=" * 60)
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
try:
|
||||
self.server.settimeout(1)
|
||||
client, addr = self.server.accept()
|
||||
handler = threading.Thread(
|
||||
target=self._handle_client,
|
||||
args=(client,),
|
||||
daemon=True
|
||||
)
|
||||
handler.start()
|
||||
except socket.timeout:
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
def _handle_client(self, client):
|
||||
"""处理客户端连接,转发到内部 VLESS 代理"""
|
||||
client_addr = None
|
||||
try:
|
||||
client_addr = client.getpeername()
|
||||
client.settimeout(30)
|
||||
|
||||
# 连接到内部 VLESS 代理
|
||||
vless_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
vless_sock.settimeout(30)
|
||||
vless_sock.connect(('127.0.0.1', self._vless_port))
|
||||
|
||||
print(f"[CONN] {client_addr} -> VLESS proxy :{self._vless_port}")
|
||||
|
||||
# 双向转发
|
||||
stop_event = threading.Event()
|
||||
|
||||
def forward(src, dst, name):
|
||||
try:
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
src.settimeout(1)
|
||||
data = src.recv(8192)
|
||||
if not data:
|
||||
break
|
||||
dst.sendall(data)
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
break
|
||||
except Exception as e:
|
||||
pass
|
||||
finally:
|
||||
stop_event.set()
|
||||
|
||||
t1 = threading.Thread(target=forward, args=(client, vless_sock, "C->V"), daemon=True)
|
||||
t2 = threading.Thread(target=forward, args=(vless_sock, client, "V->C"), daemon=True)
|
||||
t1.start()
|
||||
t2.start()
|
||||
|
||||
# 等待任一方向结束
|
||||
while not stop_event.is_set():
|
||||
time.sleep(0.1)
|
||||
|
||||
print(f"[CLOSE] {client_addr}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] {client_addr}: {e}")
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
vless_sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
self.running = False
|
||||
if self.server:
|
||||
try:
|
||||
self.server.close()
|
||||
except:
|
||||
pass
|
||||
if self._vless_proxy:
|
||||
try:
|
||||
self._vless_proxy.stop()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def start_proxy_server(
|
||||
cf_proxies: str,
|
||||
uuid: str,
|
||||
two_proxy: str = None,
|
||||
host: str = '127.0.0.1',
|
||||
port: int = 1080
|
||||
):
|
||||
"""
|
||||
启动本地代理服务器
|
||||
|
||||
Args:
|
||||
cf_proxies: Workers 地址
|
||||
uuid: VLESS UUID
|
||||
two_proxy: 第二层代理(可选)
|
||||
host: 监听地址(默认 127.0.0.1)
|
||||
port: 监听端口(默认 1080)
|
||||
|
||||
Example:
|
||||
>>> cfspider.start_proxy_server(
|
||||
... cf_proxies="https://your-workers.dev",
|
||||
... uuid="your-uuid",
|
||||
... two_proxy="us.cliproxy.io:3010:user:pass",
|
||||
... port=1080
|
||||
... )
|
||||
"""
|
||||
server = TwoProxyServer(cf_proxies, uuid, two_proxy, host, port)
|
||||
server.start()
|
||||
|
||||
|
||||
# 命令行入口
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='CFspider Two-Proxy Server')
|
||||
parser.add_argument('--cf-proxies', required=True, help='Workers address')
|
||||
parser.add_argument('--uuid', required=True, help='VLESS UUID')
|
||||
parser.add_argument('--two-proxy', help='Second layer proxy (host:port:user:pass)')
|
||||
parser.add_argument('--host', default='127.0.0.1', help='Listen host (default: 127.0.0.1)')
|
||||
parser.add_argument('--port', type=int, default=1080, help='Listen port (default: 1080)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
start_proxy_server(
|
||||
cf_proxies=args.cf_proxies,
|
||||
uuid=args.uuid,
|
||||
two_proxy=args.two_proxy,
|
||||
host=args.host,
|
||||
port=args.port
|
||||
)
|
||||
|
||||
@@ -270,21 +270,52 @@ class VlessConnection:
|
||||
class LocalVlessProxy:
|
||||
"""本地 VLESS HTTP 代理服务器"""
|
||||
|
||||
def __init__(self, ws_url, vless_uuid=None):
|
||||
def __init__(self, ws_url, vless_uuid=None, two_proxy=None):
|
||||
"""
|
||||
初始化本地代理
|
||||
|
||||
Args:
|
||||
ws_url: edgetunnel WebSocket 地址
|
||||
vless_uuid: VLESS UUID
|
||||
two_proxy: 第二层代理,格式为 "host:port:user:pass"
|
||||
例如 "us.cliproxy.io:3010:username:password"
|
||||
"""
|
||||
self.ws_url = ws_url
|
||||
self.vless_uuid = vless_uuid
|
||||
self.two_proxy = self._parse_two_proxy(two_proxy) if two_proxy else None
|
||||
self.server = None
|
||||
self.thread = None
|
||||
self.port = None
|
||||
self.running = False
|
||||
|
||||
def _parse_two_proxy(self, two_proxy):
|
||||
"""解析第二层代理配置"""
|
||||
if not two_proxy:
|
||||
return None
|
||||
|
||||
# 格式: host:port:user:pass
|
||||
parts = two_proxy.split(':')
|
||||
if len(parts) == 4:
|
||||
return {
|
||||
'host': parts[0],
|
||||
'port': int(parts[1]),
|
||||
'user': parts[2],
|
||||
'pass': parts[3]
|
||||
}
|
||||
elif len(parts) == 2:
|
||||
# 无认证: host:port
|
||||
return {
|
||||
'host': parts[0],
|
||||
'port': int(parts[1]),
|
||||
'user': None,
|
||||
'pass': None
|
||||
}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid two_proxy format: {two_proxy}\n"
|
||||
"Expected format: host:port:user:pass or host:port"
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""启动代理服务器"""
|
||||
# 找可用端口
|
||||
@@ -369,9 +400,41 @@ class LocalVlessProxy:
|
||||
def _handle_connect(self, client, host, port):
|
||||
"""处理 HTTPS CONNECT 请求"""
|
||||
try:
|
||||
# 连接到 VLESS
|
||||
vless = VlessClient(self.ws_url, self.vless_uuid)
|
||||
conn = vless.connect(host, port)
|
||||
|
||||
if self.two_proxy:
|
||||
# 使用第二层代理:通过 VLESS 连接到第二层代理
|
||||
proxy = self.two_proxy
|
||||
conn = vless.connect(proxy['host'], proxy['port'])
|
||||
|
||||
# 向第二层代理发送 CONNECT 请求
|
||||
connect_request = f"CONNECT {host}:{port} HTTP/1.1\r\n"
|
||||
connect_request += f"Host: {host}:{port}\r\n"
|
||||
|
||||
# 添加代理认证
|
||||
if proxy['user'] and proxy['pass']:
|
||||
import base64
|
||||
auth = base64.b64encode(f"{proxy['user']}:{proxy['pass']}".encode()).decode()
|
||||
connect_request += f"Proxy-Authorization: Basic {auth}\r\n"
|
||||
|
||||
connect_request += "\r\n"
|
||||
conn.send(connect_request.encode())
|
||||
|
||||
# 读取代理响应
|
||||
response = b''
|
||||
while b'\r\n\r\n' not in response:
|
||||
chunk = conn.recv(4096)
|
||||
if not chunk:
|
||||
raise Exception("Second proxy connection failed")
|
||||
response += chunk
|
||||
|
||||
# 检查代理是否连接成功
|
||||
status_line = response.split(b'\r\n')[0].decode()
|
||||
if '200' not in status_line:
|
||||
raise Exception(f"Second proxy CONNECT failed: {status_line}")
|
||||
else:
|
||||
# 直接连接目标
|
||||
conn = vless.connect(host, port)
|
||||
|
||||
# 发送连接成功
|
||||
client.sendall(b'HTTP/1.1 200 Connection Established\r\n\r\n')
|
||||
@@ -395,31 +458,64 @@ class LocalVlessProxy:
|
||||
if parsed.query:
|
||||
path += '?' + parsed.query
|
||||
|
||||
# 连接到 VLESS
|
||||
vless = VlessClient(self.ws_url, self.vless_uuid)
|
||||
conn = vless.connect(host, port)
|
||||
|
||||
# 重建请求
|
||||
lines = original_request.split(b'\r\n')
|
||||
lines[0] = f'{method} {path} HTTP/1.1'.encode('utf-8')
|
||||
|
||||
# 更新 Host 头
|
||||
new_lines = [lines[0]]
|
||||
has_host = False
|
||||
for line in lines[1:]:
|
||||
if line.lower().startswith(b'host:'):
|
||||
new_lines.append(f'Host: {host}'.encode('utf-8'))
|
||||
has_host = True
|
||||
elif line.lower().startswith(b'proxy-'):
|
||||
continue
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not has_host:
|
||||
new_lines.insert(1, f'Host: {host}'.encode('utf-8'))
|
||||
|
||||
request = b'\r\n'.join(new_lines)
|
||||
conn.send(request)
|
||||
if self.two_proxy:
|
||||
# 使用第二层代理
|
||||
proxy = self.two_proxy
|
||||
conn = vless.connect(proxy['host'], proxy['port'])
|
||||
|
||||
# 重建请求(保留完整 URL,因为是发给代理的)
|
||||
lines = original_request.split(b'\r\n')
|
||||
|
||||
# 更新请求头
|
||||
new_lines = [lines[0]] # 保持原始请求行(包含完整 URL)
|
||||
has_host = False
|
||||
for line in lines[1:]:
|
||||
if line.lower().startswith(b'host:'):
|
||||
new_lines.append(f'Host: {host}'.encode('utf-8'))
|
||||
has_host = True
|
||||
elif line.lower().startswith(b'proxy-'):
|
||||
continue # 移除原有的代理头
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not has_host:
|
||||
new_lines.insert(1, f'Host: {host}'.encode('utf-8'))
|
||||
|
||||
# 添加代理认证
|
||||
if proxy['user'] and proxy['pass']:
|
||||
import base64
|
||||
auth = base64.b64encode(f"{proxy['user']}:{proxy['pass']}".encode()).decode()
|
||||
new_lines.insert(1, f'Proxy-Authorization: Basic {auth}'.encode('utf-8'))
|
||||
|
||||
request = b'\r\n'.join(new_lines)
|
||||
conn.send(request)
|
||||
else:
|
||||
# 直接连接目标
|
||||
conn = vless.connect(host, port)
|
||||
|
||||
# 重建请求
|
||||
lines = original_request.split(b'\r\n')
|
||||
lines[0] = f'{method} {path} HTTP/1.1'.encode('utf-8')
|
||||
|
||||
# 更新 Host 头
|
||||
new_lines = [lines[0]]
|
||||
has_host = False
|
||||
for line in lines[1:]:
|
||||
if line.lower().startswith(b'host:'):
|
||||
new_lines.append(f'Host: {host}'.encode('utf-8'))
|
||||
has_host = True
|
||||
elif line.lower().startswith(b'proxy-'):
|
||||
continue
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
if not has_host:
|
||||
new_lines.insert(1, f'Host: {host}'.encode('utf-8'))
|
||||
|
||||
request = b'\r\n'.join(new_lines)
|
||||
conn.send(request)
|
||||
|
||||
# 读取响应并转发
|
||||
self._relay_response(client, conn)
|
||||
|
||||
Reference in New Issue
Block a user