mirror of
https://github.com/violettoolssite/CFspider.git
synced 2026-04-05 03:09:01 +08:00
v1.8.4: Add two-proxy support for custom exit IP region
This commit is contained in:
1
CloudflareSpeedTest
Submodule
1
CloudflareSpeedTest
Submodule
Submodule CloudflareSpeedTest added at 6eaacd6b2c
88
README.md
88
README.md
@@ -4,7 +4,7 @@
|
||||
[](https://pypi.org/project/cfspider/)
|
||||
[](LICENSE)
|
||||
|
||||
**v1.8.3** - 基于 VLESS 协议的免费代理 IP 池,利用 Cloudflare 全球 300+ 边缘节点作为出口,**完全隐藏 CF 特征**,支持隐身模式、TLS 指纹模拟、网页镜像和浏览器自动化。
|
||||
**v1.8.4** - 基于 VLESS 协议的免费代理 IP 池,利用 Cloudflare 全球 300+ 边缘节点作为出口,**完全隐藏 CF 特征**,支持隐身模式、TLS 指纹模拟、网页镜像和浏览器自动化。
|
||||
|
||||
---
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
vless://你的UUID@your-workers.dev:443?encryption=none&security=tls&type=ws&host=your-workers.dev&path=%2F你的UUID#CFspider
|
||||
```
|
||||
|
||||
## v1.8.3 新特性
|
||||
## v1.8.4 新特性
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
@@ -47,12 +47,94 @@ vless://你的UUID@your-workers.dev:443?encryption=none&security=tls&type=ws&hos
|
||||
| **v2ray 支持** | 支持 v2rayN/v2rayNG/Clash 等客户端直接使用 |
|
||||
| **动态 IP 池** | 每次请求自动获取新的出口 IP,从 300+ 全球节点选择 |
|
||||
| **固定 IP 模式** | 新增 `static_ip` 参数,支持保持同一 IP 进行多次请求 |
|
||||
| **双层代理** | 新增 `two_proxy` 参数,支持通过第二层代理出口(自定义出口 IP 地区) |
|
||||
| **UUID 安全** | 支持自定义 UUID,配置后需手动填写,默认 UUID 界面显示警告 |
|
||||
| **简化 API** | 只需填写 Workers 地址,自动获取配置 |
|
||||
|
||||
## 双层代理(指定出口 IP 地区)
|
||||
|
||||
> **需要指定出口 IP 地区?** 通过双层代理功能,可以让流量经过第二层代理服务器出口,获得特定地区的 IP 地址。
|
||||
|
||||
**流量路径:**
|
||||
```
|
||||
本地 → Cloudflare Workers (VLESS) → 第二层代理 → 目标网站
|
||||
```
|
||||
|
||||
### 为什么需要双层代理?
|
||||
|
||||
| 场景 | 默认模式 | 双层代理模式 |
|
||||
|------|---------|-------------|
|
||||
| 出口 IP | Cloudflare WARP(荷兰/美国等) | 第二层代理的 IP(可指定地区) |
|
||||
| IP 类型 | 数据中心 IP | 可选住宅/移动 IP |
|
||||
| 适用场景 | 一般爬虫、匿名访问 | 需要特定地区 IP、住宅 IP 的场景 |
|
||||
|
||||
### 使用方式
|
||||
|
||||
**方式 1:Workers 环境变量配置(推荐)**
|
||||
|
||||
在 Cloudflare Dashboard 或使用 wrangler 配置:
|
||||
|
||||
```bash
|
||||
# 格式: host:port:user:pass
|
||||
echo "us.cliproxy.io:3010:username:password" | npx wrangler secret put TWO_PROXY --name cfspider
|
||||
```
|
||||
|
||||
配置后,Python 和 v2ray 客户端都会自动使用双层代理:
|
||||
|
||||
```python
|
||||
import cfspider
|
||||
|
||||
# 自动使用 Workers 配置的双层代理
|
||||
response = cfspider.get(
|
||||
"https://httpbin.org/ip",
|
||||
cf_proxies="https://your-workers.dev",
|
||||
uuid="your-uuid"
|
||||
)
|
||||
print(response.json()) # 出口 IP 为第二层代理的 IP
|
||||
```
|
||||
|
||||
**方式 2:Python 参数指定**
|
||||
|
||||
```python
|
||||
import cfspider
|
||||
|
||||
# 手动指定第二层代理(优先级高于 Workers 配置)
|
||||
response = cfspider.get(
|
||||
"https://httpbin.org/ip",
|
||||
cf_proxies="https://your-workers.dev",
|
||||
uuid="your-uuid",
|
||||
two_proxy="us.cliproxy.io:3010:username:password"
|
||||
)
|
||||
print(response.json()) # 出口 IP 为第二层代理的 IP
|
||||
```
|
||||
|
||||
### 配置优先级
|
||||
|
||||
| Workers `TWO_PROXY` | Python `two_proxy` | 结果 |
|
||||
|---------------------|--------------------|----- |
|
||||
| ✅ 已配置 | ❌ 不传 | 自动使用 Workers 配置 |
|
||||
| ❌ 未配置 | ✅ 传入 | 使用 Python 参数 |
|
||||
| ✅ 已配置 | ✅ 传入 | **Python 参数优先** |
|
||||
| ❌ 未配置 | ❌ 不传 | 使用 Cloudflare WARP IP |
|
||||
|
||||
### 代理 IP 购买
|
||||
|
||||
如需购买高质量的代理 IP(支持住宅 IP、移动 IP、指定地区),推荐:
|
||||
|
||||
- **Cliproxy**: [https://dash.cliproxy.com/](https://dash.cliproxy.com/) - 支持 HTTP/SOCKS5 代理,覆盖全球多个地区
|
||||
|
||||
代理格式示例:
|
||||
```
|
||||
# HTTP 代理格式
|
||||
host:port:username:password
|
||||
|
||||
# 示例
|
||||
us.cliproxy.io:3010:2e75108689-region-JP:password123
|
||||
```
|
||||
|
||||
## 核心优势:VLESS 动态 IP 池
|
||||
|
||||
> **CFspider v1.8.3 采用 VLESS 协议**,每次请求自动获取新的出口 IP,自动从 300+ 全球节点中选择最优节点。**完全隐藏 Cloudflare 特征**(无 CF-Ray、CF-Worker、Cf-Connecting-Ip 等头),实现真正的匿名代理。
|
||||
> **CFspider v1.8.4 采用 VLESS 协议**,每次请求自动获取新的出口 IP,自动从 300+ 全球节点中选择最优节点。**完全隐藏 Cloudflare 特征**(无 CF-Ray、CF-Worker、Cf-Connecting-Ip 等头),实现真正的匿名代理。
|
||||
|
||||
### 动态 IP 池的优势
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "cfspider"
|
||||
version = "1.8.3"
|
||||
version = "1.8.4"
|
||||
description = "Cloudflare Workers proxy IP pool client"
|
||||
readme = "README.md"
|
||||
license = {text = "Apache-2.0"}
|
||||
|
||||
13
test.py
13
test.py
@@ -1,9 +1,8 @@
|
||||
import CodeVideoRenderer
|
||||
|
||||
CodeVideoRenderer.CameraFollowCursorCV(code_string="""
|
||||
import cfspider
|
||||
|
||||
response = cfspider.get("https://www.cfspider.com",impersonate="chrome131")
|
||||
|
||||
print(response.text)
|
||||
""")
|
||||
res = cfspider.get(
|
||||
"https://httpbin.org/ip",
|
||||
cf_proxies="https://ip.kami666.xyz",
|
||||
uuid="c373c80c-58e4-4e64-8db5-40096905ec58",
|
||||
)
|
||||
print(res.text)
|
||||
1
test_simple.js
Normal file
1
test_simple.js
Normal file
@@ -0,0 +1 @@
|
||||
export default{async fetch(){return new Response("OK")}}
|
||||
8
test_worker.js
Normal file
8
test_worker.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
return new Response('Hello World!', {
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
303
workers.js
303
workers.js
@@ -35,8 +35,10 @@ export default {
|
||||
const newIpEnabled = env.NEW_IP !== 'false' && env.NEW_IP !== '0';
|
||||
// 检测是否使用默认 UUID(未配置环境变量)
|
||||
const isDefaultUUID = !envUUID || !uuidRegex.test(envUUID);
|
||||
// 双层代理配置:格式为 host:port:user:pass 或 host:port
|
||||
const twoProxy = env.TWO_PROXY || env.two_proxy || '';
|
||||
if (cfspiderPath === '' || cfspiderPath === '/') {
|
||||
return new Response(generateCFspiderPage(request, url, 访问IP, userID, newIpEnabled, isDefaultUUID), {
|
||||
return new Response(generateCFspiderPage(request, url, 访问IP, userID, newIpEnabled, isDefaultUUID, twoProxy), {
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
||||
});
|
||||
}
|
||||
@@ -50,7 +52,7 @@ export default {
|
||||
if (cfspiderPath === 'api/status') {
|
||||
return new Response(JSON.stringify({
|
||||
status: 'online',
|
||||
version: '1.8.3',
|
||||
version: '1.8.4',
|
||||
colo: request.cf?.colo || 'unknown',
|
||||
uptime: Date.now() - (globalThis.START_TIME || Date.now())
|
||||
}), { headers: { 'Content-Type': 'application/json' } });
|
||||
@@ -59,18 +61,34 @@ export default {
|
||||
if (cfspiderPath === 'api/uuid' || cfspiderPath === 'api/config') {
|
||||
// 从环境变量读取 new_ip 设置,默认为 true
|
||||
const newIpEnabled = env.NEW_IP !== 'false' && env.NEW_IP !== '0';
|
||||
// 双层代理配置
|
||||
const twoProxyConfig = env.TWO_PROXY || env.two_proxy || '';
|
||||
// 如果配置了自定义 UUID,不公开返回(需要用户手动填写)
|
||||
// 只有使用默认 UUID 时才公开返回
|
||||
const configResponse = {
|
||||
host: url.hostname,
|
||||
new_ip: newIpEnabled,
|
||||
version: '1.8.2',
|
||||
is_default_uuid: isDefaultUUID
|
||||
version: '1.8.4',
|
||||
is_default_uuid: isDefaultUUID,
|
||||
two_proxy_enabled: !!twoProxyConfig
|
||||
};
|
||||
// 只有默认 UUID 才公开返回 uuid 字段
|
||||
if (isDefaultUUID) {
|
||||
configResponse.uuid = userID;
|
||||
configResponse.vless_path = '/' + userID;
|
||||
configResponse.vless_path = twoProxyConfig
|
||||
? '/' + userID + '?two_proxy=' + encodeURIComponent(twoProxyConfig)
|
||||
: '/' + userID;
|
||||
} else {
|
||||
// 即使不返回 uuid,也返回 vless_path(不含 uuid)供客户端解析 two_proxy
|
||||
if (twoProxyConfig) {
|
||||
configResponse.two_proxy = twoProxyConfig; // 返回完整的双层代理配置
|
||||
}
|
||||
}
|
||||
// 如果启用双层代理,返回代理主机信息
|
||||
if (twoProxyConfig) {
|
||||
const parts = twoProxyConfig.split(':');
|
||||
configResponse.two_proxy_host = parts[0] || '';
|
||||
configResponse.two_proxy_port = parts[1] || '';
|
||||
}
|
||||
return new Response(JSON.stringify(configResponse), {
|
||||
headers: {
|
||||
@@ -155,7 +173,7 @@ export default {
|
||||
检测代理响应 = await SOCKS5可用性验证('socks5', url.searchParams.get('socks5'));
|
||||
} else if (url.searchParams.has('http')) {
|
||||
检测代理响应 = await SOCKS5可用性验证('http', url.searchParams.get('http'));
|
||||
} else {
|
||||
} else {
|
||||
return new Response(JSON.stringify({ error: '缺少代理参数' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
|
||||
}
|
||||
return new Response(JSON.stringify(检测代理响应, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } });
|
||||
@@ -406,7 +424,18 @@ export default {
|
||||
} else if (!envUUID) return fetch(Pages静态页面 + '/noKV').then(r => { const headers = new Headers(r.headers); headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); return new Response(r.body, { status: 404, statusText: r.statusText, headers }); });
|
||||
} else if (管理员密码) {// ws代理
|
||||
await 反代参数获取(request);
|
||||
return await 处理WS请求(request, userID);
|
||||
// 解析 URL 中的 two_proxy 参数(双层代理)
|
||||
const urlPath = new URL(request.url).pathname;
|
||||
let twoProxyFromPath = '';
|
||||
if (urlPath.includes('two_proxy=')) {
|
||||
const match = urlPath.match(/two_proxy=([^&]+)/);
|
||||
if (match) {
|
||||
twoProxyFromPath = decodeURIComponent(decodeURIComponent(match[1]));
|
||||
}
|
||||
}
|
||||
// 优先使用 URL 参数,其次使用环境变量
|
||||
const twoProxyConfig = twoProxyFromPath || env.TWO_PROXY || env.two_proxy || '';
|
||||
return await 处理WS请求(request, userID, twoProxyConfig);
|
||||
}
|
||||
|
||||
let 伪装页URL = env.URL || 'nginx';
|
||||
@@ -436,7 +465,7 @@ export default {
|
||||
}
|
||||
};
|
||||
///////////////////////////////////////////////////////////////////////WS传输数据///////////////////////////////////////////////
|
||||
async function 处理WS请求(request, yourUUID) {
|
||||
async function 处理WS请求(request, yourUUID, twoProxy = '') {
|
||||
const wssPair = new WebSocketPair();
|
||||
const [clientSock, serverSock] = Object.values(wssPair);
|
||||
serverSock.accept();
|
||||
@@ -445,6 +474,22 @@ async function 处理WS请求(request, yourUUID) {
|
||||
const earlyData = request.headers.get('sec-websocket-protocol') || '';
|
||||
const readable = makeReadableStr(serverSock, earlyData);
|
||||
let 判断是否是木马 = null;
|
||||
|
||||
// 解析双层代理配置
|
||||
let twoProxyParsed = null;
|
||||
if (twoProxy) {
|
||||
const parts = twoProxy.split(':');
|
||||
if (parts.length >= 2) {
|
||||
twoProxyParsed = {
|
||||
hostname: parts[0],
|
||||
port: parseInt(parts[1], 10),
|
||||
username: parts[2] || '',
|
||||
password: parts[3] || ''
|
||||
};
|
||||
console.log(`[双层代理] 已启用: ${twoProxyParsed.hostname}:${twoProxyParsed.port}`);
|
||||
}
|
||||
}
|
||||
|
||||
readable.pipeTo(new WritableStream({
|
||||
async write(chunk) {
|
||||
if (isDnsQuery) return await forwardataudp(chunk, serverSock, null);
|
||||
@@ -470,7 +515,7 @@ async function 处理WS请求(request, yourUUID) {
|
||||
if (判断是否是木马) {
|
||||
const { port, hostname, rawClientData } = 解析木马请求(chunk, yourUUID);
|
||||
if (isSpeedTestSite(hostname)) throw new Error('Speedtest site is blocked');
|
||||
await forwardataTCP(hostname, port, rawClientData, serverSock, null, remoteConnWrapper, yourUUID);
|
||||
await forwardataTCP(hostname, port, rawClientData, serverSock, null, remoteConnWrapper, yourUUID, twoProxyParsed);
|
||||
} else {
|
||||
const { port, hostname, rawIndex, version, isUDP } = 解析魏烈思请求(chunk, yourUUID);
|
||||
if (isSpeedTestSite(hostname)) throw new Error('Speedtest site is blocked');
|
||||
@@ -481,7 +526,7 @@ async function 处理WS请求(request, yourUUID) {
|
||||
const respHeader = new Uint8Array([version[0], 0]);
|
||||
const rawData = chunk.slice(rawIndex);
|
||||
if (isDnsQuery) return forwardataudp(rawData, serverSock, respHeader);
|
||||
await forwardataTCP(hostname, port, rawData, serverSock, respHeader, remoteConnWrapper, yourUUID);
|
||||
await forwardataTCP(hostname, port, rawData, serverSock, respHeader, remoteConnWrapper, yourUUID, twoProxyParsed);
|
||||
}
|
||||
},
|
||||
})).catch((err) => {
|
||||
@@ -585,8 +630,8 @@ function 解析魏烈思请求(chunk, token) {
|
||||
if (!hostname) return { hasError: true, message: `Invalid address: ${addressType}` };
|
||||
return { hasError: false, addressType, port, hostname, isUDP, rawIndex: addrValIdx + addrLen, version };
|
||||
}
|
||||
async function forwardataTCP(host, portNum, rawData, ws, respHeader, remoteConnWrapper, yourUUID) {
|
||||
console.log(`[TCP转发] 目标: ${host}:${portNum} | 反代IP: ${反代IP} | 反代兜底: ${启用反代兜底 ? '是' : '否'} | 反代类型: ${启用SOCKS5反代 || 'proxyip'} | 全局: ${启用SOCKS5全局反代 ? '是' : '否'}`);
|
||||
async function forwardataTCP(host, portNum, rawData, ws, respHeader, remoteConnWrapper, yourUUID, twoProxy = null) {
|
||||
console.log(`[TCP转发] 目标: ${host}:${portNum} | 反代IP: ${反代IP} | 双层代理: ${twoProxy ? twoProxy.hostname + ':' + twoProxy.port : '否'} | 反代类型: ${启用SOCKS5反代 || 'proxyip'}`);
|
||||
|
||||
async function connectDirect(address, port, data, 所有反代数组 = null, 反代兜底 = true) {
|
||||
let remoteSock;
|
||||
@@ -646,8 +691,91 @@ async function forwardataTCP(host, portNum, rawData, ws, respHeader, remoteConnW
|
||||
connectStreams(newSocket, ws, respHeader, null);
|
||||
}
|
||||
|
||||
// 双层代理连接函数
|
||||
async function connectViaTwoProxy() {
|
||||
if (!twoProxy) throw new Error('双层代理未配置');
|
||||
console.log(`[双层代理] 通过 ${twoProxy.hostname}:${twoProxy.port} 连接到 ${host}:${portNum}`);
|
||||
|
||||
const socket = connect({ hostname: twoProxy.hostname, port: twoProxy.port });
|
||||
const writer = socket.writable.getWriter();
|
||||
const reader = socket.readable.getReader();
|
||||
|
||||
try {
|
||||
// 构建 HTTP CONNECT 请求
|
||||
const auth = twoProxy.username && twoProxy.password
|
||||
? `Proxy-Authorization: Basic ${btoa(`${twoProxy.username}:${twoProxy.password}`)}\r\n`
|
||||
: '';
|
||||
const connectRequest = `CONNECT ${host}:${portNum} HTTP/1.1\r\nHost: ${host}:${portNum}\r\n${auth}User-Agent: CFspider/1.8.3\r\nConnection: keep-alive\r\n\r\n`;
|
||||
|
||||
await writer.write(new TextEncoder().encode(connectRequest));
|
||||
|
||||
// 读取代理响应
|
||||
let responseBuffer = new Uint8Array(0);
|
||||
let headerEndIndex = -1;
|
||||
let bytesRead = 0;
|
||||
|
||||
while (headerEndIndex === -1 && bytesRead < 8192) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) throw new Error('代理连接关闭');
|
||||
responseBuffer = new Uint8Array([...responseBuffer, ...value]);
|
||||
bytesRead = responseBuffer.length;
|
||||
|
||||
// 查找 \r\n\r\n
|
||||
for (let i = 0; i < responseBuffer.length - 3; i++) {
|
||||
if (responseBuffer[i] === 0x0d && responseBuffer[i + 1] === 0x0a &&
|
||||
responseBuffer[i + 2] === 0x0d && responseBuffer[i + 3] === 0x0a) {
|
||||
headerEndIndex = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (headerEndIndex === -1) throw new Error('无效的代理响应');
|
||||
|
||||
const responseText = new TextDecoder().decode(responseBuffer.slice(0, headerEndIndex));
|
||||
const statusMatch = responseText.match(/HTTP\/\d\.\d\s+(\d+)/);
|
||||
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 0;
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new Error(`代理连接失败: HTTP ${statusCode}`);
|
||||
}
|
||||
|
||||
console.log(`[双层代理] 隧道建立成功: ${host}:${portNum}`);
|
||||
|
||||
// 发送原始数据
|
||||
await writer.write(rawData);
|
||||
writer.releaseLock();
|
||||
reader.releaseLock();
|
||||
|
||||
return socket;
|
||||
} catch (error) {
|
||||
try { writer.releaseLock(); } catch (e) { }
|
||||
try { reader.releaseLock(); } catch (e) { }
|
||||
try { socket.close(); } catch (e) { }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const 验证SOCKS5白名单 = (addr) => SOCKS5白名单.some(p => new RegExp(`^${p.replace(/\*/g, '.*')}$`, 'i').test(addr));
|
||||
if (启用SOCKS5反代 && (启用SOCKS5全局反代 || 验证SOCKS5白名单(host))) {
|
||||
|
||||
// 优先使用双层代理
|
||||
if (twoProxy) {
|
||||
console.log(`[TCP转发] 使用双层代理`);
|
||||
try {
|
||||
const proxySocket = await connectViaTwoProxy();
|
||||
remoteConnWrapper.socket = proxySocket;
|
||||
proxySocket.closed.catch(() => { }).finally(() => closeSocketQuietly(ws));
|
||||
connectStreams(proxySocket, ws, respHeader, null);
|
||||
} catch (err) {
|
||||
console.log(`[双层代理] 连接失败: ${err.message}, 回退到默认方式`);
|
||||
// 回退到默认方式
|
||||
try {
|
||||
await connecttoPry();
|
||||
} catch (err2) {
|
||||
throw err2;
|
||||
}
|
||||
}
|
||||
} else if (启用SOCKS5反代 && (启用SOCKS5全局反代 || 验证SOCKS5白名单(host))) {
|
||||
console.log(`[TCP转发] 启用 SOCKS5/HTTP 全局代理`);
|
||||
try {
|
||||
await connecttoPry();
|
||||
@@ -780,7 +908,7 @@ function base64ToArray(b64Str) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return { earlyData: bytes.buffer, error: null };
|
||||
} catch (error) {
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
@@ -2162,7 +2290,7 @@ function generateIPPool(request) {
|
||||
return { pool: ipPool, total: ipPool.length, online: ipPool.length };
|
||||
}
|
||||
|
||||
function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = true, isDefaultUUID = false) {
|
||||
function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = true, isDefaultUUID = false, twoProxy = '') {
|
||||
const colo = request.cf?.colo || 'UNKNOWN';
|
||||
const country = request.cf?.country || 'XX';
|
||||
const city = request.cf?.city || 'Night City';
|
||||
@@ -2173,13 +2301,32 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
const longitude = request.cf?.longitude || '0';
|
||||
const continent = request.cf?.continent || 'XX';
|
||||
const lang = url.searchParams.get('lang') || 'zh';
|
||||
const VERSION = '1.8.3';
|
||||
const VERSION = '1.8.4';
|
||||
|
||||
// 解析双层代理配置
|
||||
let twoProxyHost = '', twoProxyPort = '', twoProxyUser = '', twoProxyPass = '', twoProxyEnabled = false;
|
||||
if (twoProxy) {
|
||||
const parts = twoProxy.split(':');
|
||||
if (parts.length >= 2) {
|
||||
twoProxyHost = parts[0];
|
||||
twoProxyPort = parts[1];
|
||||
twoProxyUser = parts[2] || '';
|
||||
twoProxyPass = parts[3] || '';
|
||||
twoProxyEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// VLESS 配置
|
||||
const vlessHost = url.hostname;
|
||||
const vlessPort = '443';
|
||||
const vlessPath = '/' + userID;
|
||||
const vlessLink = userID ? `vless://${userID}@${vlessHost}:${vlessPort}?security=tls&type=ws&host=${vlessHost}&sni=${vlessHost}&path=${encodeURIComponent(vlessPath)}&encryption=none#CFspider-${colo}` : '';
|
||||
// 如果启用双层代理,在 path 中携带代理信息
|
||||
const vlessPath = twoProxyEnabled
|
||||
? '/' + userID + '?two_proxy=' + encodeURIComponent(twoProxy)
|
||||
: '/' + userID;
|
||||
const vlessLink = userID ? `vless://${userID}@${vlessHost}:${vlessPort}?security=tls&type=ws&host=${vlessHost}&sni=${vlessHost}&path=${encodeURIComponent(vlessPath)}&encryption=none#CFspider-${colo}${twoProxyEnabled ? '-2P' : ''}` : '';
|
||||
|
||||
// 双层代理专用链接(出口为第二层代理 IP)
|
||||
const twoProxyLink = twoProxyEnabled && userID ? `vless://${userID}@${vlessHost}:${vlessPort}?security=tls&type=ws&host=${vlessHost}&sni=${vlessHost}&path=${encodeURIComponent(vlessPath)}&encryption=none#CFspider-TwoProxy-${twoProxyHost.split('.')[0]}` : '';
|
||||
|
||||
const countryNames = {
|
||||
'JP': '日本', 'CN': '中国', 'US': '美国', 'HK': '香港', 'TW': '台湾',
|
||||
@@ -2213,7 +2360,17 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
newIp: '动态 IP', transport: '传输协议', security: '安全协议', encryption: '加密方式',
|
||||
credits: 'VLESS 技术基于 edgetunnel 项目',
|
||||
v2rayClients: '支持的客户端',
|
||||
copySuccess: '已复制!'
|
||||
copySuccess: '已复制!',
|
||||
twoProxyTitle: '双层代理配置',
|
||||
twoProxyHost: '第二层代理',
|
||||
twoProxyPort: '代理端口',
|
||||
twoProxyAuth: '认证信息',
|
||||
twoProxyEnabled: '已启用',
|
||||
twoProxyDisabled: '未配置',
|
||||
twoProxyLink: '双层代理链接',
|
||||
twoProxyDesc: '流量路径: 本地 → Workers (VLESS) → 第二层代理 → 目标网站',
|
||||
twoProxyEnvHint: '设置环境变量 TWO_PROXY 启用双层代理',
|
||||
exitIp: '出口 IP'
|
||||
},
|
||||
en: {
|
||||
subtitle: 'Cloudflare VLESS Proxy Network',
|
||||
@@ -2231,7 +2388,17 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
newIp: 'Dynamic IP', transport: 'Transport', security: 'Security', encryption: 'Encryption',
|
||||
credits: 'VLESS based on edgetunnel project',
|
||||
v2rayClients: 'Supported Clients',
|
||||
copySuccess: 'Copied!'
|
||||
copySuccess: 'Copied!',
|
||||
twoProxyTitle: 'Two-Layer Proxy',
|
||||
twoProxyHost: 'Second Proxy',
|
||||
twoProxyPort: 'Proxy Port',
|
||||
twoProxyAuth: 'Authentication',
|
||||
twoProxyEnabled: 'Enabled',
|
||||
twoProxyDisabled: 'Not Configured',
|
||||
twoProxyLink: 'Two-Proxy Link',
|
||||
twoProxyDesc: 'Traffic: Local → Workers (VLESS) → Second Proxy → Target',
|
||||
twoProxyEnvHint: 'Set TWO_PROXY env variable to enable',
|
||||
exitIp: 'Exit IP'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2637,11 +2804,11 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<div class="vless-header">
|
||||
<div class="vless-title-main">
|
||||
<span>VLESS PROXY</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vless-status">
|
||||
<div class="status-pill active"><span class="status-dot"></span> ${t.online}</div>
|
||||
<div class="status-pill">${t.newIp}: ON</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${isDefaultUUID ? `
|
||||
@@ -2651,7 +2818,7 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<div class="warning-title">Security Notice</div>
|
||||
<div class="warning-text">${t.defaultUuidWarning}</div>
|
||||
<div class="warning-path">Dashboard → Workers → Settings → Variables → Add "UUID"</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
@@ -2661,40 +2828,40 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.vlessHost}</span>
|
||||
<span class="config-value">${vlessHost}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.vlessPort}</span>
|
||||
<span class="config-value">443</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.transport}</span>
|
||||
<span class="config-value">WebSocket</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.security}</span>
|
||||
<span class="config-value success">TLS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card">
|
||||
<div class="config-card-title">Authentication</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.vlessUUID}</span>
|
||||
<span class="config-value uuid">${userID}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">Path</span>
|
||||
<span class="config-value">/${userID.substring(0,8)}...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">${t.encryption}</span>
|
||||
<span class="config-value">none</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-label">Status</span>
|
||||
<span class="config-value success">${isDefaultUUID ? 'Public' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vless-link-box" onclick="copyVlessLink(this)">
|
||||
<span class="copy-hint" id="copyHint">${t.vlessCopy}</span>
|
||||
@@ -2702,8 +2869,8 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<span>${t.vlessLink}</span>
|
||||
</div>
|
||||
<div class="vless-link-text" id="vlessLink">${vlessLink}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clients-section">
|
||||
<div class="clients-title">${t.v2rayClients}</div>
|
||||
<div class="clients-grid">
|
||||
@@ -2715,8 +2882,49 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<span class="client-tag">NekoRay</span>
|
||||
<span class="client-tag">Surge</span>
|
||||
<span class="client-tag">Quantumult X</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Two-Proxy Section -->
|
||||
${userID ? `
|
||||
<div class="two-proxy-section" style="background: ${twoProxyEnabled ? 'linear-gradient(135deg, rgba(63,185,80,0.15) 0%, rgba(88,166,255,0.15) 100%)' : 'var(--bg-secondary)'}; border: ${twoProxyEnabled ? '2px solid var(--accent-green)' : '1px solid var(--border-color)'}; border-radius: 16px; padding: 24px; margin-bottom: 32px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 12px;">
|
||||
<div style="font-family: 'Orbitron', sans-serif; font-size: 1.2rem; color: ${twoProxyEnabled ? 'var(--accent-green)' : 'var(--text-secondary)'}; display: flex; align-items: center; gap: 10px;">
|
||||
<span>🔗</span>
|
||||
<span>${t.twoProxyTitle}</span>
|
||||
</div>
|
||||
<div class="status-pill ${twoProxyEnabled ? 'active' : ''}" style="background: var(--bg-tertiary); border: 1px solid ${twoProxyEnabled ? 'var(--accent-green)' : 'var(--border-color)'}; padding: 6px 14px; border-radius: 20px; font-size: 0.8rem;">
|
||||
${twoProxyEnabled ? '<span class="status-dot" style="width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green); animation: pulse 2s infinite; display: inline-block; margin-right: 6px;"></span>' : ''}
|
||||
${twoProxyEnabled ? t.twoProxyEnabled : t.twoProxyDisabled}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${twoProxyEnabled ? `
|
||||
<div style="background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 8px; padding: 16px; margin-bottom: 16px;">
|
||||
<div style="font-size: 0.8rem; color: var(--accent-cyan); margin-bottom: 8px;">${t.twoProxyDesc}</div>
|
||||
<div style="display: flex; gap: 24px; flex-wrap: wrap; font-size: 0.9rem;">
|
||||
<div><span style="color: var(--text-secondary);">${t.twoProxyHost}:</span> <span style="color: var(--accent-green);">${twoProxyHost}</span></div>
|
||||
<div><span style="color: var(--text-secondary);">${t.twoProxyPort}:</span> <span style="color: var(--text-primary);">${twoProxyPort}</span></div>
|
||||
<div><span style="color: var(--text-secondary);">${t.twoProxyAuth}:</span> <span style="color: ${twoProxyUser ? 'var(--accent-yellow)' : 'var(--text-secondary)'};">${twoProxyUser ? twoProxyUser.substring(0, 8) + '***' : 'None'}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vless-link-box" onclick="copyTwoProxyLink(this)" style="background: var(--bg-primary); border: 2px solid var(--accent-green); border-radius: 12px; padding: 20px; cursor: pointer; transition: all 0.3s; position: relative;">
|
||||
<span class="copy-hint" id="copyHint2" style="position: absolute; top: 16px; right: 16px; font-size: 0.75rem; color: var(--text-secondary); background: var(--bg-tertiary); padding: 4px 10px; border-radius: 4px;">${t.vlessCopy}</span>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
||||
<span>🚀</span>
|
||||
<span>${t.twoProxyLink} (${t.exitIp}: ${twoProxyHost})</span>
|
||||
</div>
|
||||
<div class="vless-link-text" id="twoProxyLinkText" style="font-size: 0.85rem; color: var(--accent-green); word-break: break-all; line-height: 1.8; padding: 12px; background: var(--bg-secondary); border-radius: 8px; user-select: all;">${twoProxyLink}</div>
|
||||
</div>
|
||||
` : `
|
||||
<div style="background: var(--bg-primary); border: 1px dashed var(--border-color); border-radius: 8px; padding: 20px; text-align: center;">
|
||||
<div style="font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 8px;">${t.twoProxyEnvHint}</div>
|
||||
<div style="font-size: 0.8rem; color: var(--accent-cyan); font-family: 'JetBrains Mono', monospace;">TWO_PROXY=host:port:user:pass</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
@@ -2739,14 +2947,14 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
<div class="info-row"><span class="info-label">${t.city}</span><span class="info-value">${cityName}</span></div>
|
||||
<div class="info-row"><span class="info-label">${t.asn}</span><span class="info-value">AS${asn}</span></div>
|
||||
<div class="info-row"><span class="info-label">${t.timezone}</span><span class="info-value">${timezone}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-panel">
|
||||
<div class="panel-title">${t.visitorInfoTitle}</div>
|
||||
<div class="info-row"><span class="info-label">${t.visitorIP}</span><span class="info-value">${visitorIP}</span></div>
|
||||
<div class="info-row"><span class="info-label">${t.country}</span><span class="info-value">${countryName}</span></div>
|
||||
<div class="info-row"><span class="info-label">${t.asn}</span><span class="info-value">AS${asn}</span></div>
|
||||
<div class="info-row"><span class="info-label">${t.coordinates}</span><span class="info-value">${latitude}, ${longitude}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Code Example -->
|
||||
@@ -2756,11 +2964,11 @@ function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = tr
|
||||
|
||||
<span class="code-comment"># ${lang === 'zh' ? '使用 CFspider 代理池获取不同 IP' : 'Use CFspider proxy pool for different IPs'}</span>
|
||||
<span class="code-keyword">for</span> i <span class="code-keyword">in</span> range(<span class="code-string">5</span>):
|
||||
response = cfspider.<span class="code-function">get</span>(
|
||||
<span class="code-string">"https://httpbin.org/ip"</span>,
|
||||
response = cfspider.<span class="code-function">get</span>(
|
||||
<span class="code-string">"https://httpbin.org/ip"</span>,
|
||||
cf_proxies=<span class="code-string">"https://your-workers.dev"</span>${isDefaultUUID ? '' : `,
|
||||
uuid=<span class="code-string">"your-uuid-here"</span>`}
|
||||
)
|
||||
)
|
||||
<span class="code-function">print</span>(response.json())
|
||||
|
||||
<span class="code-comment"># ${lang === 'zh' ? '固定 IP 模式 - 保持同一出口 IP' : 'Static IP mode - keep same IP'}</span>
|
||||
@@ -2809,6 +3017,23 @@ response = cfspider.<span class="code-function">get</span>(
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function copyTwoProxyLink(el) {
|
||||
const linkEl = document.getElementById('twoProxyLinkText');
|
||||
if (!linkEl) return;
|
||||
const link = linkEl.innerText;
|
||||
const hint = document.getElementById('copyHint2');
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
hint.textContent = '${t.copySuccess}';
|
||||
hint.style.background = 'var(--accent-green)';
|
||||
hint.style.color = '#fff';
|
||||
setTimeout(() => {
|
||||
hint.textContent = '${t.vlessCopy}';
|
||||
hint.style.background = 'var(--bg-tertiary)';
|
||||
hint.style.color = 'var(--text-secondary)';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
4
wrangler_test.toml
Normal file
4
wrangler_test.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
name = "cfspider-test"
|
||||
main = "test_simple.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
workers_dev = true
|
||||
Reference in New Issue
Block a user