v1.8.4: Add two-proxy support for custom exit IP region

This commit is contained in:
violettools
2026-01-22 17:22:28 +08:00
parent fffb675838
commit 5da73175ec
13 changed files with 929 additions and 118 deletions

1
CloudflareSpeedTest Submodule

Submodule CloudflareSpeedTest added at 6eaacd6b2c

View File

@@ -4,7 +4,7 @@
[![Python](https://img.shields.io/pypi/pyversions/cfspider)](https://pypi.org/project/cfspider/)
[![License](https://img.shields.io/github/license/violettoolssite/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 的场景 |
### 使用方式
**方式 1Workers 环境变量配置(推荐)**
在 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
```
**方式 2Python 参数指定**
```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 池的优势

View File

@@ -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",
]

View File

@@ -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():

View File

@@ -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
View 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
)

View File

@@ -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)

View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1 @@
export default{async fetch(){return new Response("OK")}}

8
test_worker.js Normal file
View File

@@ -0,0 +1,8 @@
export default {
async fetch(request, env) {
return new Response('Hello World!', {
headers: { 'Content-Type': 'text/plain' }
});
}
};

View File

@@ -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
View File

@@ -0,0 +1,4 @@
name = "cfspider-test"
main = "test_simple.js"
compatibility_date = "2024-01-01"
workers_dev = true