From 696ddf43ea3bb73c6b1599800542cc5c2e465324 Mon Sep 17 00:00:00 2001 From: test01 Date: Wed, 21 Jan 2026 22:04:07 +0800 Subject: [PATCH] v1.8.2: Simplified API, UUID security enhancement, auto new IP - Simplified API: Only Workers URL needed, auto-fetch UUID config - UUID security: Custom UUID not exposed in /api/config, requires manual uuid parameter - Default UUID warning: Shows security warning when using default UUID - Auto new IP: Each request automatically gets new exit IP - Dynamic code examples: Workers UI shows correct Python code based on UUID config - Updated README with UUID configuration guide --- .gitignore | 16 +- README.md | 117 +- cfspider/__init__.py | 49 +- cfspider/api.py | 936 +++++------ cfspider/browser.py | 118 +- cfspider/export.py | 3 +- cfspider/session.py | 175 ++- cfspider/stealth.py | 184 ++- pyproject.toml | 12 +- workers.js | 3556 +++++++++++++++++++++++++++--------------- 10 files changed, 3132 insertions(+), 2034 deletions(-) diff --git a/.gitignore b/.gitignore index 25fba4d..edf36a6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,18 @@ obfuscate_pages.py obfuscate_config.json #示例文件 -examples/ \ No newline at end of file +examples/ + +#视频生成脚本 +create_video.py +temp_obfuscate.js + +#视频文件(排除普通版本,保留高亮模糊版本) +media/videos/1080p60/CameraFollowCursorCVScene.mp4 +# 允许提交高亮模糊版本 +!media/videos/1080p60/CameraFollowCursorCV.mp4 + +#视频文件目录 +media/images/ +media/text/ +media/videos/1080p60/partial_movie_files/ diff --git a/README.md b/README.md index 3c60fbd..e38a903 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## ⚡ 核心优势:动态 IP 池 -> **CFspider 是动态 IP 池**,每次请求可能使用不同的 Cloudflare IP,自动从 300+ 全球节点中选择最优节点。 +> **CFspider 是动态 IP 池**,每次请求自动获取新的出口 IP,自动从 300+ 全球节点中选择最优节点。完全隐藏 Cloudflare 特征(无 CF-Ray、CF-Worker 等头),实现真正的匿名代理。 ### 🎯 动态 IP 池的优势 @@ -21,10 +21,10 @@ # 静态 IP 代理:固定 IP,容易被封 proxies = {"http": "1.2.3.4:8080"} # 固定 IP -# CFspider 动态 IP 池:每次请求可能不同 -response = cfspider.get("https://example.com", cf_proxies="your-workers.dev") -print(response.cf_colo) # 可能显示 NRT, SIN, LAX 等不同节点 -# 每次请求可能使用不同的 Cloudflare IP +# CFspider 动态 IP 池:每次请求自动获取新 IP +response = cfspider.get("https://example.com", cf_proxies="https://your-workers.dev") +print(response.json()['origin']) # 每次都是不同的出口 IP +# 完全隐藏 CF 特征,目标网站无法检测到使用了 Cloudflare ``` ## 📸 项目截图 @@ -196,15 +196,16 @@ Cloudflare Workers 免费版每日 100,000 请求,无需信用卡,无需付 ``` **工作流程:** -1. 你的应用调用 `cfspider.get(url, cf_proxies="workers.dev")` -2. CFspider 发送请求到你的 Cloudflare Workers +1. 你的应用调用 `cfspider.get(url, cf_proxies="https://your-workers.dev")` +2. CFspider 通过 VLESS 协议连接到你的 Cloudflare Workers 3. Workers 自动路由到离目标网站最近的边缘节点(动态 IP 池) -4. 每次请求可能使用不同的 Cloudflare IP(从 300+ 节点中选择) -5. 响应返回,目标网站看到的是 Cloudflare IP,而不是你的 IP +4. 每次请求自动获取新的出口 IP(从 300+ 节点中选择) +5. 响应返回,目标网站看到的是干净的请求(无 CF-Ray、CF-Worker 等头) ## 特性 -- **动态 IP 池**:每次请求可能使用不同的 Cloudflare IP,从 300+ 全球节点自动选择 +- **动态 IP 池**:每次请求自动获取新的出口 IP,从 300+ 全球节点自动选择 +- **完全隐藏 CF 特征**:使用 VLESS 协议,目标网站无法检测到 CF-Ray、CF-Worker 等 Cloudflare 头 - 使用 Cloudflare 全球 300+ 边缘节点 IP - 与 requests 库语法一致,无学习成本 - 支持 GET、POST、PUT、DELETE 等所有 HTTP 方法 @@ -263,39 +264,42 @@ Cloudflare Workers 免费版每日 100,000 请求,无需信用卡,无需付 如需自定义域名,可在 Worker → Settings → Triggers → Custom Domain 中添加。 -### Token 鉴权配置(可选) +### UUID 配置(推荐) -为了增强安全性,你可以为 Workers 配置 Token 鉴权: +为了增强安全性,强烈建议配置自定义 UUID: 1. 在 Worker → Settings → Variables and Secrets 中添加环境变量 -2. 变量名:`TOKEN` -3. 变量值:你的 token(支持多个 token,用逗号分隔,如 `token1,token2,token3`) +2. 变量名:`UUID` +3. 变量值:你的 UUID(标准 UUID 格式,如 `xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx`) 4. 保存并重新部署 Worker -配置 Token 后,所有 API 请求(除了首页和 debug 页面)都需要提供有效的 token: +**UUID 与 Python 库的关系:** + +| Workers 配置 | Python 库用法 | +|-------------|--------------| +| 未配置 `UUID` 环境变量(使用默认 UUID) | 不需要填写 `uuid` 参数,直接使用 `cfspider.get(url, cf_proxies="...")` | +| 配置了自定义 `UUID` 环境变量 | **必须**填写 `uuid` 参数:`cfspider.get(url, cf_proxies="...", uuid="你的UUID")` | + +**示例:** ```python import cfspider -# 在请求时传递 token +# 如果 Workers 使用默认 UUID(未配置环境变量) +response = cfspider.get("https://httpbin.org/ip", cf_proxies="https://your-workers.dev") + +# 如果 Workers 配置了自定义 UUID 环境变量 response = cfspider.get( "https://httpbin.org/ip", cf_proxies="https://your-workers.dev", - token="your-token" # 从查询参数传递 + uuid="xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx" # 必须填写配置的 UUID ) - -# 或在 Session 中设置 token -with cfspider.Session( - cf_proxies="https://your-workers.dev", - token="your-token" -) as session: - response = session.get("https://httpbin.org/ip") ``` **注意:** -- 如果不配置 `TOKEN` 环境变量,则所有请求都可以访问(无鉴权) -- Token 可以通过查询参数 `?token=xxx` 或 Header `Authorization: Bearer xxx` 传递 -- 支持配置多个 token,用逗号分隔 +- 如果不配置 `UUID` 环境变量,Workers 会使用默认 UUID,界面会显示安全警告 +- 强烈建议在生产环境中配置自定义 UUID +- 配置自定义 UUID 后,Python 库请求时必须提供相同的 UUID,否则无法连接 ## 安装 @@ -360,11 +364,13 @@ cfspider install ```python import cfspider -cf_proxies = "https://your-workers.dev" - -response = cfspider.get("https://httpbin.org/ip", cf_proxies=cf_proxies) -print(response.text) -# {"origin": "2a06:98c0:3600::103, 172.71.24.151"} # Cloudflare IP +# 只需填写 Workers 地址,每次请求自动获取新 IP +for i in range(5): + response = cfspider.get( + "https://httpbin.org/ip", + cf_proxies="https://your-workers.dev" + ) + print(response.json()['origin']) # 每次都是不同的 IP ``` ### 浏览器模式 @@ -372,26 +378,17 @@ print(response.text) ```python import cfspider -# 使用本地 HTTP 代理 -browser = cfspider.Browser(cf_proxies="127.0.0.1:9674") +# 简化用法:只需 Workers 地址(自动获取 UUID) +browser = cfspider.Browser(cf_proxies="https://your-workers.dev") html = browser.html("https://httpbin.org/ip") -print(html) +print(html) # 返回动态 IP browser.close() -# 使用 VLESS 链接(推荐,无需填写 UUID) +# 使用 VLESS 链接 browser = cfspider.Browser( cf_proxies="vless://your-uuid@v2.example.com:443?path=/" ) html = browser.html("https://httpbin.org/ip") -print(html) # 返回 Cloudflare IP -browser.close() - -# 使用 edgetunnel 域名 + UUID(旧方式) -browser = cfspider.Browser( - cf_proxies="v2.example.com", - vless_uuid="your-vless-uuid" -) -html = browser.html("https://httpbin.org/ip") browser.close() # 无代理模式 @@ -763,13 +760,13 @@ with cfspider.StealthSession( ```python import cfspider -# 隐身模式 + Cloudflare IP 出口 +# 隐身模式 + 动态 IP(每次请求自动获取新 IP) response = cfspider.get( "https://httpbin.org/headers", cf_proxies="https://your-workers.dev", stealth=True ) -print(response.cf_colo) # Cloudflare 节点代码 +print(response.json()) # 完整的浏览器请求头 # 隐身会话 + Workers 代理 with cfspider.StealthSession( @@ -777,7 +774,7 @@ with cfspider.StealthSession( browser='chrome' ) as session: r1 = session.get("https://example.com") - r2 = session.get("https://example.com/api") + r2 = session.get("https://example.com/api") # 自动携带 Cookie 和 Referer ``` ### 配合 TLS 指纹模拟 @@ -1354,25 +1351,19 @@ cfspider install ```python import cfspider -# 1. HTTP 代理(IP:PORT 格式) -browser = cfspider.Browser(cf_proxies="127.0.0.1:9674") +# 1. CFspider Workers(推荐,自动获取 UUID) +browser = cfspider.Browser(cf_proxies="https://your-workers.dev") -# 2. HTTP 代理(完整格式) -browser = cfspider.Browser(cf_proxies="http://127.0.0.1:9674") - -# 3. SOCKS5 代理 -browser = cfspider.Browser(cf_proxies="socks5://127.0.0.1:1080") - -# 4. VLESS 链接(推荐,无需填写 UUID) +# 2. VLESS 链接 browser = cfspider.Browser(cf_proxies="vless://uuid@v2.example.com:443?path=/") -# 5. edgetunnel 域名 + UUID(旧方式) -browser = cfspider.Browser( - cf_proxies="v2.example.com", - vless_uuid="your-vless-uuid" -) +# 3. HTTP 代理 +browser = cfspider.Browser(cf_proxies="http://127.0.0.1:9674") -# 6. 无代理 +# 4. SOCKS5 代理 +browser = cfspider.Browser(cf_proxies="socks5://127.0.0.1:1080") + +# 5. 无代理 browser = cfspider.Browser() ``` diff --git a/cfspider/__init__.py b/cfspider/__init__.py index 031f8a8..6162603 100644 --- a/cfspider/__init__.py +++ b/cfspider/__init__.py @@ -52,7 +52,7 @@ CFspider - Cloudflare 代理 IP 池 Python 库 from .api import ( get, post, put, delete, head, options, patch, request, - clear_map_records, get_map_collector + clear_map_records, get_map_collector, stop_vless_proxies ) from .session import Session from .cli import install_browser @@ -101,45 +101,50 @@ from .stealth import ( # 延迟导入 Browser,避免强制依赖 playwright -def Browser(cf_proxies=None, headless=True, timeout=30, vless_uuid=None): +def Browser(cf_proxies=None, headless=True, timeout=30, uuid=None): """ - 创建浏览器实例 + 创建浏览器实例 / Create browser instance + + 封装 Playwright,支持通过 Cloudflare Workers 代理浏览器流量。 + Wraps Playwright with Cloudflare Workers proxy support. Args: - cf_proxies: 代理地址,支持以下格式: - - VLESS 链接: "vless://uuid@host:port?path=/xxx#name"(推荐) - - HTTP 代理: "http://ip:port" 或 "ip:port" - - SOCKS5 代理: "socks5://ip:port" - - edgetunnel 域名: "v2.example.com"(需配合 vless_uuid) - 如不指定,则直接使用本地网络 - headless: 是否无头模式,默认 True - timeout: 请求超时时间(秒),默认 30 - vless_uuid: VLESS UUID,仅当使用域名方式时需要指定 - 如果使用完整 VLESS 链接,则无需此参数 + cf_proxies (str, optional): 代理地址 / Proxy address + - 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" + 不填则直接使用本地网络 / None for direct connection + headless (bool): 是否无头模式,默认 True / Headless mode (default: True) + timeout (int): 请求超时时间(秒),默认 30 / Timeout in seconds (default: 30) + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + / VLESS UUID (optional, auto-fetched) Returns: - Browser: 浏览器实例 + Browser: 浏览器实例 / Browser instance Example: >>> import cfspider - >>> # 使用完整 VLESS 链接(推荐,无需 vless_uuid) + >>> + >>> # 简化用法(推荐):只需 Workers 地址,自动获取 UUID >>> browser = cfspider.Browser( - ... cf_proxies="vless://uuid@v2.example.com:443?path=/" + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" ... ) >>> html = browser.html("https://example.com") >>> browser.close() >>> - >>> # 使用域名 + UUID(旧方式) + >>> # 手动指定 UUID >>> browser = cfspider.Browser( - ... cf_proxies="v2.example.com", - ... vless_uuid="your-vless-uuid" + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... uuid="c373c80c-58e4-4e64-8db5-40096905ec58" ... ) >>> >>> # 直接使用(无代理) >>> browser = cfspider.Browser() """ from .browser import Browser as _Browser - return _Browser(cf_proxies, headless, timeout, vless_uuid) + return _Browser(cf_proxies, headless, timeout, uuid) def parse_vless_link(vless_link): @@ -205,11 +210,11 @@ class PlaywrightNotInstalledError(CFSpiderError): pass -__version__ = "1.8.0" +__version__ = "1.8.2" __all__ = [ # 同步 API (requests) "get", "post", "put", "delete", "head", "options", "patch", "request", - "Session", "Browser", "install_browser", "parse_vless_link", + "Session", "Browser", "install_browser", "parse_vless_link", "stop_vless_proxies", "CFSpiderError", "BrowserNotInstalledError", "PlaywrightNotInstalledError", # 异步 API (httpx) "aget", "apost", "aput", "adelete", "ahead", "aoptions", "apatch", diff --git a/cfspider/api.py b/cfspider/api.py index bf2a686..f894c5e 100644 --- a/cfspider/api.py +++ b/cfspider/api.py @@ -2,7 +2,7 @@ CFspider 核心 API 模块 提供同步 HTTP 请求功能,支持: -- 通过 Cloudflare Workers 代理请求 +- 通过 Cloudflare Workers VLESS 代理请求 - TLS 指纹模拟 (curl_cffi) - HTTP/2 支持 (httpx) - 隐身模式(完整浏览器请求头) @@ -11,7 +11,7 @@ CFspider 核心 API 模块 import requests import time -from urllib.parse import urlencode, quote +from urllib.parse import urlencode, quote, urlparse from typing import Optional, Any # 延迟导入 IP 地图模块 @@ -78,10 +78,9 @@ class CFSpiderResponse: raise_for_status(): 当状态码非 2xx 时抛出 HTTPError Example: - >>> response = cfspider.get("https://httpbin.org/ip", cf_proxies="...") + >>> response = cfspider.get("https://httpbin.org/ip", cf_proxies="...", uuid="...") >>> print(response.status_code) # 200 >>> print(response.cf_colo) # NRT (东京节点) - >>> print(response.cf_ray) # 8a1b2c3d4e5f-NRT >>> data = response.json() >>> print(data['origin']) # Cloudflare IP """ @@ -380,62 +379,45 @@ class CFSpiderResponse: return save_response(self.content, filepath, encoding=encoding) -def request(method, url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): + stealth=False, stealth_browser='chrome', delay=None, **kwargs): """ 发送 HTTP 请求 / Send HTTP request - 这是 CFspider 的核心函数,支持多种代理模式和反爬虫功能。 - This is the core function of CFspider, supporting multiple proxy modes and anti-crawler features. + 通过 CFspider Workers VLESS 代理发送请求,完全隐藏 Cloudflare 特征。 + Send requests through CFspider Workers VLESS proxy, completely hiding Cloudflare signatures. + 每次请求自动获取新的出口 IP。 + Automatically gets a new exit IP for each request. Args: method (str): HTTP 方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH) / HTTP method (GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH) url (str): 目标 URL,必须包含协议(https://) / Target URL (must include protocol, e.g., https://) - cf_proxies (str, optional): 代理地址,根据 cf_workers 参数有不同含义 - / Proxy address, meaning depends on cf_workers parameter - - 当 cf_workers=True 时:填写 CFspider Workers 地址(如 "https://your-workers.dev") - - When cf_workers=True: CFspider Workers address (e.g., "https://your-workers.dev") - - 当 cf_workers=False 时:填写普通 HTTP/SOCKS5 代理(如 "http://127.0.0.1:8080") - - When cf_workers=False: Regular HTTP/SOCKS5 proxy (e.g., "http://127.0.0.1:8080") + cf_proxies (str, optional): CFspider Workers 地址 + / CFspider Workers address + - 如 "https://cfspider.violetqqcom.workers.dev" + - e.g., "https://cfspider.violetqqcom.workers.dev" - 不填写时:直接请求目标 URL,不使用代理 - None: Direct request without proxy - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - - True: cf_proxies 是 Workers 地址,请求通过 Workers API 转发 - - True: cf_proxies is Workers address, requests forwarded via Workers API - - False: cf_proxies 是普通代理,使用 requests/httpx 的 proxies 参数 - - False: cf_proxies is regular proxy, uses requests/httpx proxies parameter + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + - 不填写时会自动从 Workers 首页获取 UUID + - When not provided, UUID will be fetched from Workers homepage http2 (bool): 是否启用 HTTP/2 协议(默认 False) / Whether to enable HTTP/2 protocol (default: False) - - True: 使用 httpx 客户端,支持 HTTP/2 - - True: Uses httpx client with HTTP/2 support - - False: 使用 requests 库(默认行为) - - False: Uses requests library (default behavior) - - 注意:http2 和 impersonate 不能同时使用 - - Note: http2 and impersonate cannot be used together impersonate (str, optional): TLS 指纹模拟,模拟真实浏览器的 TLS 握手特征 / TLS fingerprint impersonation, mimics real browser TLS handshake - 可选值:chrome131, chrome124, safari18_0, firefox133, edge101 等 - Options: chrome131, chrome124, safari18_0, firefox133, edge101, etc. - - 设置后自动使用 curl_cffi 发送请求 - - Automatically uses curl_cffi when set - - 完整列表:cfspider.get_supported_browsers() - - Full list: cfspider.get_supported_browsers() map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) / Whether to generate IP map HTML file (default: False) - - True: 请求完成后生成包含代理 IP 信息的交互式地图 - - True: Generates interactive map with proxy IP information after request map_file (str): 地图输出文件名(默认 "cfspider_map.html") / Map output filename (default: "cfspider_map.html") stealth (bool): 是否启用隐身模式(默认 False) / Whether to enable stealth mode (default: False) - True: 自动添加 15+ 个完整浏览器请求头,模拟真实浏览器访问 - True: Automatically adds 15+ complete browser headers, mimics real browser - - 添加的请求头包括:User-Agent, Accept, Accept-Language, Sec-Fetch-*, Sec-CH-UA 等 - - Headers include: User-Agent, Accept, Accept-Language, Sec-Fetch-*, Sec-CH-UA, etc. stealth_browser (str): 隐身模式使用的浏览器类型(默认 'chrome') / Stealth mode browser type (default: 'chrome') - 可选值:chrome, firefox, safari, edge, chrome_mobile @@ -444,85 +426,61 @@ def request(method, url, cf_proxies=None, cf_workers=True, http2=False, imperson / Random delay range before request (seconds) - 如 (1, 3) 表示请求前随机等待 1-3 秒 - e.g., (1, 3) means random wait 1-3 seconds before request - - 用于模拟人类行为,避免被反爬系统检测 - - Used to simulate human behavior, avoid anti-crawler detection - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - - 当使用 Workers API(cf_workers=True)时,将 token 添加到查询参数 - - When using Workers API (cf_workers=True), adds token to query parameters - - 如果 Workers 端配置了 TOKEN 环境变量,必须提供有效的 token - - Required when Workers has TOKEN environment variable configured - - 格式:从查询参数 ?token=xxx 传递 - - Format: Passed via query parameter ?token=xxx **kwargs: 其他参数,与 requests 库完全兼容 / Other parameters, fully compatible with requests library - params (dict): URL 查询参数 / URL query parameters - - headers (dict): 自定义请求头(会与隐身模式头合并) - / Custom headers (merged with stealth mode headers) + - headers (dict): 自定义请求头 / Custom headers - data (dict/str): 表单数据 / Form data - - json (dict): JSON 数据(自动设置 Content-Type) - / JSON data (Content-Type set automatically) + - json (dict): JSON 数据 / JSON data - cookies (dict): Cookie - - timeout (int/float): 超时时间(秒),默认 30 - / Timeout (seconds), default: 30 - - allow_redirects (bool): 是否跟随重定向,默认 True - / Whether to follow redirects, default: True - - verify (bool): 是否验证 SSL 证书,默认 True - / Whether to verify SSL certificate, default: True + - timeout (int/float): 超时时间(秒),默认 30 / Timeout (seconds), default: 30 Returns: - CFSpiderResponse: 响应对象,包含以下属性 - / Response object with the following attributes + CFSpiderResponse: 响应对象 / Response object - text: 响应文本 / Response text - content: 响应字节 / Response bytes - json(): 解析 JSON / Parse JSON - status_code: HTTP 状态码 / HTTP status code - headers: 响应头 / Response headers - - cf_colo: Cloudflare 节点代码(使用 Workers 时可用) - / Cloudflare colo code (available when using Workers) - - cf_ray: Cloudflare Ray ID - - Raises: - ImportError: 当需要的可选依赖未安装时 - / When required optional dependencies are not installed - - http2=True 需要 httpx[http2] / http2=True requires httpx[http2] - - impersonate 需要 curl_cffi / impersonate requires curl_cffi - ValueError: 当 http2 和 impersonate 同时启用时 - / When http2 and impersonate are both enabled - requests.RequestException: 网络请求失败时 - / When network request fails Examples: >>> import cfspider >>> - >>> # 基本 GET 请求 + >>> # 基本 GET 请求(无代理) >>> response = cfspider.get("https://httpbin.org/ip") >>> print(response.json()) >>> - >>> # 使用 Workers 代理 + >>> # 使用 Workers VLESS 代理(自动获取 UUID,每次新 IP) >>> response = cfspider.get( ... "https://httpbin.org/ip", - ... cf_proxies="https://your-workers.dev" + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" ... ) - >>> print(response.cf_colo) # NRT, SIN, LAX 等 + >>> print(response.json()) # 出口 IP >>> - >>> # 隐身模式 + TLS 指纹 - >>> response = cfspider.get( - ... "https://example.com", - ... stealth=True, - ... impersonate="chrome131" - ... ) - - Notes: - - http2 和 impersonate 使用不同的后端(httpx/curl_cffi),不能同时启用 - - 隐身模式的请求头优先级:用户自定义 > stealth 默认头 - - 使用 Workers 代理时,自定义请求头通过 X-CFSpider-Header-* 传递 + >>> # 多次请求自动获取不同 IP + >>> for i in range(5): + ... response = cfspider.get( + ... "https://httpbin.org/ip", + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" + ... ) + ... print(response.json()['origin']) # 每次都是不同 IP """ # 应用随机延迟 if delay: from .stealth import random_delay random_delay(delay[0], delay[1]) + # 如果指定了 cf_proxies,使用 VLESS 代理 + if cf_proxies: + return _request_vless( + method, url, cf_proxies, uuid, + http2=http2, impersonate=impersonate, + map_output=map_output, map_file=map_file, + stealth=stealth, stealth_browser=stealth_browser, + **kwargs + ) + + # 没有指定代理,直接请求 params = kwargs.pop("params", None) headers = kwargs.pop("headers", {}) @@ -530,10 +488,10 @@ def request(method, url, cf_proxies=None, cf_workers=True, http2=False, imperson if stealth: from .stealth import get_stealth_headers stealth_headers = get_stealth_headers(stealth_browser) - # 用户自定义的 headers 优先级更高 final_headers = stealth_headers.copy() final_headers.update(headers) headers = final_headers + data = kwargs.pop("data", None) json_data = kwargs.pop("json", None) cookies = kwargs.pop("cookies", None) @@ -544,109 +502,54 @@ def request(method, url, cf_proxies=None, cf_workers=True, http2=False, imperson # 如果指定了 impersonate,使用 curl_cffi if impersonate: - response = _request_impersonate( - method, url, cf_proxies, cf_workers, impersonate, - params=params, headers=headers, data=data, - json_data=json_data, cookies=cookies, timeout=timeout, - token=token, **kwargs + curl_requests = _get_curl_cffi() + response = curl_requests.request( + method, + url, + params=params, + headers=headers, + data=data, + json=json_data, + cookies=cookies, + timeout=timeout, + impersonate=impersonate, + **kwargs ) - _handle_map_output(response, url, start_time, map_output, map_file) - return response + resp = CFSpiderResponse(response) + _handle_map_output(resp, url, start_time, map_output, map_file) + return resp # 如果启用 HTTP/2,使用 httpx if http2: - response = _request_httpx( - method, url, cf_proxies, cf_workers, - params=params, headers=headers, data=data, - json_data=json_data, cookies=cookies, timeout=timeout, - token=token, **kwargs - ) - _handle_map_output(response, url, start_time, map_output, map_file) - return response + httpx = _get_httpx() + with httpx.Client(http2=True, timeout=timeout) as client: + response = client.request( + method, + url, + params=params, + headers=headers, + data=data, + json=json_data, + cookies=cookies, + **kwargs + ) + resp = CFSpiderResponse(response) + _handle_map_output(resp, url, start_time, map_output, map_file) + return resp - # 如果没有指定 cf_proxies,直接使用 requests - if not cf_proxies: - resp = requests.request( - method, - url, - params=params, - headers=headers, - data=data, - json=json_data, - cookies=cookies, - timeout=timeout, - **kwargs - ) - response = CFSpiderResponse(resp) - _handle_map_output(response, url, start_time, map_output, map_file) - return response - - # cf_workers=False:使用普通代理 - if not cf_workers: - # 处理代理格式 - proxy_url = cf_proxies - if not proxy_url.startswith(('http://', 'https://', 'socks5://')): - proxy_url = f"http://{proxy_url}" - - proxies = { - "http": proxy_url, - "https": proxy_url - } - - resp = requests.request( - method, - url, - params=params, - headers=headers, - data=data, - json=json_data, - cookies=cookies, - timeout=timeout, - proxies=proxies, - **kwargs - ) - response = CFSpiderResponse(resp) - _handle_map_output(response, url, start_time, map_output, map_file) - return response - - # cf_workers=True:使用 CFspider Workers API 代理 - cf_proxies_url = cf_proxies.rstrip("/") - - # 确保有协议前缀 - if not cf_proxies_url.startswith(('http://', 'https://')): - cf_proxies_url = f"https://{cf_proxies_url}" - - target_url = url - if params: - target_url = f"{url}?{urlencode(params)}" - - # 构建代理 URL,添加 token 参数(如果提供) - proxy_url = f"{cf_proxies_url}/proxy?url={quote(target_url, safe='')}&method={method.upper()}" - if token: - proxy_url += f"&token={quote(token, safe='')}" - - request_headers = {} - if headers: - for key, value in headers.items(): - request_headers[f"X-CFSpider-Header-{key}"] = value - - if cookies: - cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) - request_headers["X-CFSpider-Header-Cookie"] = cookie_str - - resp = requests.post( - proxy_url, - headers=request_headers, + # 默认使用 requests + resp = requests.request( + method, + url, + params=params, + headers=headers, data=data, json=json_data, + cookies=cookies, timeout=timeout, **kwargs ) - - cf_colo = resp.headers.get("X-CF-Colo") - cf_ray = resp.headers.get("CF-Ray") - - response = CFSpiderResponse(resp, cf_colo=cf_colo, cf_ray=cf_ray) + response = CFSpiderResponse(resp) _handle_map_output(response, url, start_time, map_output, map_file) return response @@ -662,7 +565,7 @@ def _handle_map_output(response, url, start_time, map_output, map_file): # 收集 IP 记录 ip_map.add_ip_record( url=url, - ip=None, # 无法直接获取 IP,但有 cf_colo + ip=None, cf_colo=getattr(response, 'cf_colo', None), cf_ray=getattr(response, 'cf_ray', None), status_code=response.status_code, @@ -673,14 +576,170 @@ def _handle_map_output(response, url, start_time, map_output, map_file): ip_map.generate_map_html(output_file=map_file) -def _request_impersonate(method, url, cf_proxies, cf_workers, impersonate, - params=None, headers=None, data=None, json_data=None, - cookies=None, timeout=30, token=None, **kwargs): - """使用 curl_cffi 发送请求(支持 TLS 指纹模拟)""" - curl_requests = _get_curl_cffi() +# VLESS 本地代理缓存 +_vless_proxy_cache = {} + +# Workers 配置缓存 +_workers_config_cache = {} + + +def _get_workers_config(cf_proxies): + """ + 从 Workers 获取配置(UUID、new_ip 等) - # 如果没有指定 cf_proxies,直接请求 - if not cf_proxies: + 返回: + dict: { + 'uuid': str, + 'host': str, + 'new_ip': bool, + ... + } + """ + # 解析 Workers 地址 + parsed = urlparse(cf_proxies) + if parsed.scheme: + host = parsed.netloc or parsed.path.split('/')[0] + else: + host = cf_proxies.split('/')[0] + + # 检查缓存 + if host in _workers_config_cache: + return _workers_config_cache[host] + + # 尝试从 Workers API 获取配置 + try: + workers_url = f"https://{host}" + resp = requests.get(f"{workers_url}/api/config", timeout=10) + if resp.status_code == 200: + config = resp.json() + config['host'] = host + _workers_config_cache[host] = config + return config + except: + pass + + # 尝试从 /api/uuid 获取(兼容旧版本) + try: + workers_url = f"https://{host}" + resp = requests.get(f"{workers_url}/api/uuid", timeout=10) + if resp.status_code == 200: + config = resp.json() + config['host'] = host + if 'new_ip' not in config: + config['new_ip'] = True # 默认启用 + _workers_config_cache[host] = config + return config + except: + pass + + # 如果获取失败,尝试从首页 HTML 解析 UUID + try: + workers_url = f"https://{host}" + resp = requests.get(workers_url, timeout=10) + if resp.status_code == 200: + import re + match = re.search(r'([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})', resp.text) + if match: + config = { + 'uuid': match.group(1).lower(), + 'host': host, + 'new_ip': True # 默认启用 + } + _workers_config_cache[host] = config + return config + except: + pass + + return None + + +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', **kwargs): + """ + 使用 VLESS 协议发送请求 + + 通过 CFspider Workers 的 VLESS 协议代理请求, + 完全隐藏 Cloudflare 特征(CF-Ray、CF-Worker 等头)。 + 每次请求自动获取新的出口 IP。 + + Args: + method: HTTP 方法 + url: 目标 URL + cf_proxies: Workers 地址 + uuid: VLESS UUID(可选,不填则自动获取) + 其他参数与 request() 相同 + """ + from .vless_client import LocalVlessProxy + import uuid as uuid_mod + + # 获取 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 可获取) + if not uuid: + uuid = config.get('uuid') + + if not uuid: + # Workers 配置了自定义 UUID,需要用户手动填写 + raise ValueError( + f"Workers 配置了自定义 UUID,需要手动指定 uuid 参数。\n" + "用法: cfspider.get(url, cf_proxies='...', uuid='你的UUID')\n" + "提示: UUID 可在 Workers 界面或 Cloudflare Dashboard 中查看。" + ) + + # 构建 VLESS WebSocket URL: wss://host/uuid + vless_url = f"wss://{host}/{uuid}" + + # 每次请求创建新连接,确保获取新 IP + cache_key = f"{host}:{uuid}:{uuid_mod.uuid4()}" + + # 创建本地 VLESS 代理(不缓存,每次新连接) + proxy = LocalVlessProxy(vless_url, uuid) + port = proxy.start() + + # 构建本地代理 URL + local_proxy = f"http://127.0.0.1:{port}" + + # 记录请求开始时间 + start_time = time.time() + + # 准备请求参数 + params = kwargs.pop("params", None) + headers = kwargs.pop("headers", {}) + + # 如果启用隐身模式,添加完整的浏览器请求头 + if stealth: + from .stealth import get_stealth_headers + stealth_headers = get_stealth_headers(stealth_browser) + final_headers = stealth_headers.copy() + final_headers.update(headers) + headers = final_headers + + data = kwargs.pop("data", None) + json_data = kwargs.pop("json", None) + cookies = kwargs.pop("cookies", None) + timeout = kwargs.pop("timeout", 30) + + proxies = { + "http": local_proxy, + "https": local_proxy + } + + # 如果指定了 impersonate,使用 curl_cffi + if impersonate: + curl_requests = _get_curl_cffi() response = curl_requests.request( method, url, @@ -691,456 +750,191 @@ def _request_impersonate(method, url, cf_proxies, cf_workers, impersonate, cookies=cookies, timeout=timeout, impersonate=impersonate, + proxies=proxies, **kwargs ) - return CFSpiderResponse(response) + resp = CFSpiderResponse(response) + _handle_map_output(resp, url, start_time, map_output, map_file) + return resp - # cf_workers=False:使用普通代理 - if not cf_workers: - proxy_url = cf_proxies - if not proxy_url.startswith(('http://', 'https://', 'socks5://')): - proxy_url = f"http://{proxy_url}" - - response = curl_requests.request( - method, - url, - params=params, - headers=headers, - data=data, - json=json_data, - cookies=cookies, - timeout=timeout, - impersonate=impersonate, - proxies={"http": proxy_url, "https": proxy_url}, - **kwargs - ) - return CFSpiderResponse(response) + # 如果启用 HTTP/2,使用 httpx + if http2: + httpx = _get_httpx() + with httpx.Client(http2=True, timeout=timeout, proxy=local_proxy) as client: + response = client.request( + method, + url, + params=params, + headers=headers, + data=data, + json=json_data, + cookies=cookies, + **kwargs + ) + resp = CFSpiderResponse(response) + _handle_map_output(resp, url, start_time, map_output, map_file) + return resp - # cf_workers=True:使用 CFspider Workers API 代理 - cf_proxies = cf_proxies.rstrip("/") - - if not cf_proxies.startswith(('http://', 'https://')): - cf_proxies = f"https://{cf_proxies}" - - target_url = url - if params: - target_url = f"{url}?{urlencode(params)}" - - proxy_url = f"{cf_proxies}/proxy?url={quote(target_url, safe='')}&method={method.upper()}" - if token: - proxy_url += f"&token={quote(token, safe='')}" - - request_headers = {} - if headers: - for key, value in headers.items(): - request_headers[f"X-CFSpider-Header-{key}"] = value - - if cookies: - cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) - request_headers["X-CFSpider-Header-Cookie"] = cookie_str - - response = curl_requests.post( - proxy_url, - headers=request_headers, + # 默认使用 requests + resp = requests.request( + method, + url, + params=params, + headers=headers, data=data, json=json_data, + cookies=cookies, timeout=timeout, - impersonate=impersonate, + proxies=proxies, **kwargs ) - - cf_colo = response.headers.get("X-CF-Colo") - cf_ray = response.headers.get("CF-Ray") - - return CFSpiderResponse(response, cf_colo=cf_colo, cf_ray=cf_ray) + response = CFSpiderResponse(resp) + _handle_map_output(response, url, start_time, map_output, map_file) + return response -def _request_httpx(method, url, cf_proxies, cf_workers, params=None, headers=None, - data=None, json_data=None, cookies=None, timeout=30, token=None, **kwargs): - """使用 httpx 发送请求(支持 HTTP/2)""" - httpx = _get_httpx() +def stop_vless_proxies(): + """ + 停止所有 VLESS 本地代理 - # 如果没有指定 cf_proxies,直接请求 - if not cf_proxies: - with httpx.Client(http2=True, timeout=timeout) as client: - response = client.request( - method, - url, - params=params, - headers=headers, - data=data, - json=json_data, - cookies=cookies, - **kwargs - ) - return CFSpiderResponse(response) + 在程序结束时调用,释放资源。 - # cf_workers=False:使用普通代理 - if not cf_workers: - proxy_url = cf_proxies - if not proxy_url.startswith(('http://', 'https://', 'socks5://')): - proxy_url = f"http://{proxy_url}" - - with httpx.Client(http2=True, timeout=timeout, proxy=proxy_url) as client: - response = client.request( - method, - url, - params=params, - headers=headers, - data=data, - json=json_data, - cookies=cookies, - **kwargs - ) - return CFSpiderResponse(response) - - # cf_workers=True:使用 CFspider Workers API 代理 - cf_proxies = cf_proxies.rstrip("/") - - if not cf_proxies.startswith(('http://', 'https://')): - cf_proxies = f"https://{cf_proxies}" - - target_url = url - if params: - target_url = f"{url}?{urlencode(params)}" - - proxy_url = f"{cf_proxies}/proxy?url={quote(target_url, safe='')}&method={method.upper()}" - if token: - proxy_url += f"&token={quote(token, safe='')}" - - request_headers = {} - if headers: - for key, value in headers.items(): - request_headers[f"X-CFSpider-Header-{key}"] = value - - if cookies: - cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) - request_headers["X-CFSpider-Header-Cookie"] = cookie_str - - with httpx.Client(http2=True, timeout=timeout) as client: - response = client.post( - proxy_url, - headers=request_headers, - data=data, - json=json_data, - **kwargs - ) - - cf_colo = response.headers.get("X-CF-Colo") - cf_ray = response.headers.get("CF-Ray") - - return CFSpiderResponse(response, cf_colo=cf_colo, cf_ray=cf_ray) + Example: + >>> import cfspider + >>> # 使用 VLESS 发送请求 + >>> response = cfspider.get("https://httpbin.org/ip", + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... uuid="c373c80c-58e4-4e64-8db5-40096905ec58" + ... ) + >>> # 程序结束时清理 + >>> cfspider.stop_vless_proxies() + """ + for key, proxy in list(_vless_proxy_cache.items()): + try: + proxy.stop() + except: + pass + _vless_proxy_cache.clear() -def get(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): + stealth=False, stealth_browser='chrome', delay=None, **kwargs): """ 发送 GET 请求 / Send GET request + 每次请求自动获取新的出口 IP。 + Automatically gets a new exit IP for each request. + Args: url (str): 目标 URL / Target URL (must include protocol, e.g., https://) - cf_proxies (str, optional): 代理地址 / Proxy address - - 当 cf_workers=True 时:CFspider Workers 地址(如 "https://your-workers.dev") - - When cf_workers=True: CFspider Workers address (e.g., "https://your-workers.dev") - - 当 cf_workers=False 时:普通 HTTP/SOCKS5 代理(如 "http://127.0.0.1:8080") - - When cf_workers=False: Regular HTTP/SOCKS5 proxy (e.g., "http://127.0.0.1:8080") + cf_proxies (str, optional): CFspider Workers 地址 / CFspider Workers address + - 如 "https://cfspider.violetqqcom.workers.dev" - 不填写时:直接请求,不使用代理 / None: Direct request without proxy - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + - 不填写会自动从 Workers 首页获取 + - When not provided, UUID is auto-fetched from Workers http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation + impersonate (str, optional): TLS 指纹模拟 - 可选值:chrome131, chrome124, safari18_0, firefox133, edge101 等 - - Options: chrome131, chrome124, safari18_0, firefox133, edge101, etc. - - 设置后自动使用 curl_cffi 发送请求 - - Automatically uses curl_cffi when set map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - - True: 自动添加 15+ 个完整浏览器请求头 - - True: Automatically adds 15+ complete browser headers stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - - 可选值:chrome, firefox, safari, edge, chrome_mobile - - Options: chrome, firefox, safari, edge, chrome_mobile delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - - 当 Workers 配置了 TOKEN 环境变量时必填 - - Required when Workers has TOKEN environment variable configured **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - params (dict): URL 查询参数 / URL query parameters - - headers (dict): 自定义请求头 / Custom headers - - data (dict/str): 表单数据 / Form data - - json (dict): JSON 数据 / JSON data - - cookies (dict): Cookie - - timeout (int/float): 超时时间(秒),默认 30 / Timeout (seconds), default: 30 Returns: - CFSpiderResponse: 响应对象 / Response object - - text: 响应文本 / Response text - - content: 响应字节 / Response bytes - - json(): 解析 JSON / Parse JSON - - status_code: HTTP 状态码 / HTTP status code - - headers: 响应头 / Response headers - - cf_colo: Cloudflare 节点代码(使用 Workers 时可用) - / Cloudflare colo code (available when using Workers) - - cf_ray: Cloudflare Ray ID + CFSpiderResponse: 响应对象 + + Examples: + >>> import cfspider + >>> + >>> # 使用 Workers 代理(自动获取 UUID,每次新 IP) + >>> response = cfspider.get( + ... "https://httpbin.org/ip", + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" + ... ) + >>> + >>> # 多次请求自动获取不同 IP + >>> for i in range(5): + ... response = cfspider.get( + ... "https://httpbin.org/ip", + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" + ... ) + ... print(response.json()['origin']) # 每次都是不同 IP """ - return request("GET", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + 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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def post(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 POST 请求 / Send POST request - - Args: - url (str): 目标 URL / Target URL (must include protocol, e.g., https://) - cf_proxies (str, optional): 代理地址 / Proxy address - - 当 cf_workers=True 时:CFspider Workers 地址(如 "https://your-workers.dev") - - When cf_workers=True: CFspider Workers address (e.g., "https://your-workers.dev") - - 当 cf_workers=False 时:普通 HTTP/SOCKS5 代理(如 "http://127.0.0.1:8080") - - When cf_workers=False: Regular HTTP/SOCKS5 proxy (e.g., "http://127.0.0.1:8080") - - 不填写时:直接请求,不使用代理 / None: Direct request without proxy - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - - 可选值:chrome131, chrome124, safari18_0, firefox133, edge101 等 - - Options: chrome131, chrome124, safari18_0, firefox133, edge101, etc. - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - - 可选值:chrome, firefox, safari, edge, chrome_mobile - - Options: chrome, firefox, safari, edge, chrome_mobile - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - - 当 Workers 配置了 TOKEN 环境变量时必填 - - Required when Workers has TOKEN environment variable configured - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - data (dict/str): 表单数据 / Form data - - json (dict): JSON 数据 / JSON data - - headers (dict): 自定义请求头 / Custom headers - - cookies (dict): Cookie - - timeout (int/float): 超时时间(秒),默认 30 / Timeout (seconds), default: 30 - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("POST", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def put(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 PUT 请求 / Send PUT request - - Args: - url (str): 目标 URL / Target URL - cf_proxies (str, optional): 代理地址 / Proxy address - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("PUT", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def delete(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 DELETE 请求 / Send DELETE request - - Args: - url (str): 目标 URL / Target URL - cf_proxies (str, optional): 代理地址 / Proxy address - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("DELETE", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def head(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 HEAD 请求 / Send HEAD request - - Args: - url (str): 目标 URL / Target URL - cf_proxies (str, optional): 代理地址 / Proxy address - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("HEAD", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def options(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 OPTIONS 请求 / Send OPTIONS request - - Args: - url (str): 目标 URL / Target URL - cf_proxies (str, optional): 代理地址 / Proxy address - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("OPTIONS", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) -def patch(url, cf_proxies=None, cf_workers=True, http2=False, impersonate=None, +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, token=None, **kwargs): - """ - 发送 PATCH 请求 / Send PATCH request - - Args: - url (str): 目标 URL / Target URL - cf_proxies (str, optional): 代理地址 / Proxy address - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) - / Whether to use CFspider Workers API (default: True) - http2 (bool): 是否启用 HTTP/2 协议(默认 False) - / Whether to enable HTTP/2 protocol (default: False) - impersonate (str, optional): TLS 指纹模拟 / TLS fingerprint impersonation - map_output (bool): 是否生成 IP 地图 HTML 文件(默认 False) - / Whether to generate IP map HTML file (default: False) - map_file (str): 地图输出文件名(默认 "cfspider_map.html") - / Map output filename (default: "cfspider_map.html") - stealth (bool): 是否启用隐身模式(默认 False) - / Whether to enable stealth mode (default: False) - stealth_browser (str): 隐身模式浏览器类型(默认 'chrome') - / Stealth mode browser type (default: 'chrome') - delay (tuple, optional): 请求前随机延迟范围(秒),如 (1, 3) - / Random delay range before request (seconds), e.g., (1, 3) - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - **kwargs: 其他参数,与 requests 库完全兼容 - / Other parameters, fully compatible with requests library - - Returns: - CFSpiderResponse: 响应对象 / Response object - """ - return request("PATCH", url, cf_proxies=cf_proxies, cf_workers=cf_workers, + stealth=False, stealth_browser='chrome', delay=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, token=token, **kwargs) + stealth=stealth, stealth_browser=stealth_browser, delay=delay, + **kwargs) def clear_map_records(): diff --git a/cfspider/browser.py b/cfspider/browser.py index 291d7bd..221cc03 100644 --- a/cfspider/browser.py +++ b/cfspider/browser.py @@ -89,14 +89,16 @@ class PlaywrightNotInstalledError(Exception): class Browser: """ - CFspider 浏览器类 + CFspider 浏览器类 / CFspider Browser class 封装 Playwright,支持通过 Cloudflare Workers (edgetunnel) 代理浏览器流量 + Wraps Playwright with Cloudflare Workers (edgetunnel) proxy support Example: >>> import cfspider - >>> # 通过 edgetunnel Workers 代理 - >>> browser = cfspider.Browser(cf_proxies="wss://v2.kami666.xyz") + >>> + >>> # 简化用法:只需 Workers 地址(自动获取 UUID) + >>> browser = cfspider.Browser(cf_proxies="https://cfspider.violetqqcom.workers.dev") >>> html = browser.html("https://example.com") >>> browser.close() >>> @@ -106,34 +108,38 @@ class Browser: >>> browser.close() """ - def __init__(self, cf_proxies=None, headless=True, timeout=30, vless_uuid=None): + def __init__(self, cf_proxies=None, headless=True, timeout=30, uuid=None): """ - 初始化浏览器 + 初始化浏览器 / Initialize browser Args: - cf_proxies: 代理地址(选填),支持以下格式: - - VLESS 链接: "vless://uuid@host:port?path=/xxx#name"(推荐) - - HTTP 代理: "http://ip:port" 或 "ip:port" - - SOCKS5 代理: "socks5://ip:port" - - edgetunnel 域名: "v2.example.com"(需配合 vless_uuid) - 不填则直接使用本地网络 - headless: 是否无头模式,默认 True - timeout: 请求超时时间(秒),默认 30 - vless_uuid: VLESS UUID(选填),使用域名方式时需要指定 - 如果使用完整 VLESS 链接,则无需此参数 + cf_proxies (str, optional): 代理地址 / Proxy address + - 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" + 不填则直接使用本地网络 / None for direct connection + headless (bool): 是否无头模式,默认 True / Headless mode (default: True) + timeout (int): 请求超时时间(秒),默认 30 / Timeout in seconds (default: 30) + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + / VLESS UUID (optional, auto-fetched) Examples: - # 使用完整 VLESS 链接(推荐,无需填写 vless_uuid) - browser = Browser(cf_proxies="vless://uuid@v2.example.com:443?path=/") - - # 使用域名 + UUID(旧方式) - browser = Browser(cf_proxies="v2.example.com", vless_uuid="your-uuid") - - # 使用 HTTP 代理 - browser = Browser(cf_proxies="127.0.0.1:8080") - - # 使用 SOCKS5 代理 - browser = Browser(cf_proxies="socks5://127.0.0.1:1080") + >>> # 简化用法(推荐) + >>> browser = Browser(cf_proxies="https://cfspider.violetqqcom.workers.dev") + >>> + >>> # 手动指定 UUID + >>> browser = Browser( + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... uuid="c373c80c-58e4-4e64-8db5-40096905ec58" + ... ) + >>> + >>> # 使用 VLESS 链接 + >>> browser = Browser(cf_proxies="vless://uuid@v2.example.com:443?path=/") + >>> + >>> # 使用 HTTP 代理 + >>> browser = Browser(cf_proxies="127.0.0.1:8080") """ if not PLAYWRIGHT_AVAILABLE: raise PlaywrightNotInstalledError( @@ -158,20 +164,60 @@ class Browser: proxy_url = f"http://127.0.0.1:{port}" # 2. HTTP/SOCKS5 代理格式 elif cf_proxies.startswith('http://') or cf_proxies.startswith('https://') or cf_proxies.startswith('socks5://'): - proxy_url = cf_proxies + # 如果是 CFspider Workers URL,尝试获取 UUID + if 'workers.dev' in cf_proxies or not uuid: + uuid = uuid or self._get_workers_uuid(cf_proxies) + if uuid: + # 使用 VLESS 代理 + hostname = cf_proxies.replace('https://', '').replace('http://', '').split('/')[0] + 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: + # 直接使用 HTTP 代理 + proxy_url = cf_proxies # 3. IP:PORT 格式 elif ':' in cf_proxies and cf_proxies.replace('.', '').replace(':', '').isdigit(): proxy_url = f"http://{cf_proxies}" - # 4. 域名 + UUID(旧方式) - elif vless_uuid: - hostname = cf_proxies.replace('https://', '').replace('http://', '').replace('wss://', '').replace('ws://', '').split('/')[0] - ws_url = f'wss://{hostname}/{vless_uuid}' - self._vless_proxy = LocalVlessProxy(ws_url, vless_uuid) - port = self._vless_proxy.start() - proxy_url = f"http://127.0.0.1:{port}" - # 5. 默认当作 HTTP 代理 + # 4. 域名方式(尝试自动获取 UUID) else: - proxy_url = f"http://{cf_proxies}" + hostname = cf_proxies.replace('wss://', '').replace('ws://', '').split('/')[0] + uuid = uuid or self._get_workers_uuid(f"https://{hostname}") + 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}" + + def _get_workers_uuid(self, workers_url): + """从 Workers 获取 UUID / Get UUID from Workers""" + import requests + import re + + try: + # 尝试从 /api/config 获取 + config_url = f"{workers_url.rstrip('/')}/api/config" + resp = requests.get(config_url, timeout=10) + if resp.status_code == 200: + config = resp.json() + return config.get('uuid') + except: + pass + + try: + # 尝试从首页 HTML 解析 + resp = requests.get(workers_url, timeout=10) + if resp.status_code == 200: + match = re.search(r'([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})', resp.text) + if match: + return match.group(1).lower() + except: + pass + + return None # 启动 Playwright self._playwright = sync_playwright().start() diff --git a/cfspider/export.py b/cfspider/export.py index caead96..8a64c25 100644 --- a/cfspider/export.py +++ b/cfspider/export.py @@ -334,7 +334,8 @@ def export_sqlite(data: Union[Dict, List[Dict]], # 插入数据 placeholders = ", ".join(["?" for _ in fieldnames]) - insert_sql = f"INSERT INTO {table} ({', '.join([f'\"{n}\"' for n in fieldnames])}) VALUES ({placeholders})" + fieldnames_str = ', '.join([f'"{n}"' for n in fieldnames]) + insert_sql = f"INSERT INTO {table} ({fieldnames_str}) VALUES ({placeholders})" for row in rows: if isinstance(row, dict): diff --git a/cfspider/session.py b/cfspider/session.py index 7fb52a6..47e71a2 100644 --- a/cfspider/session.py +++ b/cfspider/session.py @@ -2,6 +2,7 @@ CFspider Session 模块 提供会话管理功能,在多个请求之间保持代理配置、请求头和 Cookie。 +简化 API:只需提供 Workers 地址即可自动获取 UUID 和配置。 """ from .api import request @@ -17,26 +18,27 @@ class Session: Suitable for scenarios requiring login state or consecutive requests. Attributes: - cf_proxies (str): Workers 代理地址 / Workers proxy address + cf_proxies (str): Workers 代理地址(自动获取 UUID 配置) + / Workers proxy address (auto-fetches UUID config) + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + / VLESS UUID (optional, auto-fetched if not provided) headers (dict): 会话级别的默认请求头 / Session-level default headers cookies (dict): 会话级别的 Cookie / Session-level cookies - token (str, optional): Workers API 鉴权 token / Workers API authentication token Example: >>> import cfspider >>> - >>> # 创建会话 / Create session - >>> with cfspider.Session(cf_proxies="https://your-workers.dev", token="your-token") as session: - ... # 设置会话级别的请求头 / Set session-level headers - ... session.headers['Authorization'] = 'Bearer token' - ... - ... # 所有请求都会使用相同的代理和请求头 - ... # All requests use the same proxy and headers - ... response1 = session.get("https://api.example.com/user") - ... response2 = session.post("https://api.example.com/data", json={"key": "value"}) - ... - ... # Cookie 会自动保持 / Cookies are automatically maintained - ... print(session.cookies) + >>> # 简化用法:只需 Workers 地址(自动获取 UUID) + >>> with cfspider.Session(cf_proxies="https://cfspider.violetqqcom.workers.dev") as session: + ... response = session.get("https://api.example.com/user") + ... print(f"Cookies: {session.cookies}") + >>> + >>> # 手动指定 UUID + >>> with cfspider.Session( + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... uuid="c373c80c-58e4-4e64-8db5-40096905ec58" + ... ) as session: + ... response = session.get("https://httpbin.org/ip") Note: 如果需要隐身模式的会话一致性(自动 Referer、随机延迟等), @@ -45,39 +47,145 @@ class Session: please use cfspider.StealthSession. """ - def __init__(self, cf_proxies=None, token=None): + def __init__(self, cf_proxies=None, uuid=None): """ 初始化会话 / Initialize session Args: cf_proxies (str): Workers 代理地址(必填) / Workers proxy address (required) - 例如:"https://your-workers.dev" - e.g., "https://your-workers.dev" - token (str, optional): Workers API 鉴权 token - / Workers API authentication token - 当 Workers 端配置了 TOKEN 环境变量时,必须提供有效的 token - Required when Workers has TOKEN environment variable configured + 例如:"https://cfspider.violetqqcom.workers.dev" + e.g., "https://cfspider.violetqqcom.workers.dev" + UUID 将自动从 Workers 获取 + UUID will be auto-fetched from Workers + uuid (str, optional): VLESS UUID(可选) + 如果不填写,会自动从 Workers 首页获取 + If not provided, will be auto-fetched from Workers homepage Raises: ValueError: 当 cf_proxies 为空时 - / When cf_proxies is empty Example: - >>> session = cfspider.Session(cf_proxies="https://your-workers.dev", token="your-token") + >>> # 简化用法(推荐) + >>> session = cfspider.Session(cf_proxies="https://cfspider.violetqqcom.workers.dev") + >>> + >>> # 手动指定 UUID + >>> session = cfspider.Session( + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... uuid="c373c80c-58e4-4e64-8db5-40096905ec58" + ... ) """ if not cf_proxies: raise ValueError( "cf_proxies 是必填参数。\n" "请提供 CFspider Workers 地址,例如:\n" - " session = cfspider.Session(cf_proxies='https://your-workers.dev')\n\n" + " session = cfspider.Session(cf_proxies='https://cfspider.violetqqcom.workers.dev')\n\n" + "UUID 将自动从 Workers 获取,无需手动指定。\n" "如果不需要代理,可以直接使用 cfspider.get() 等函数。\n" "如果需要隐身模式会话,请使用 cfspider.StealthSession。" ) - self.cf_proxies = cf_proxies.rstrip("/") - self.token = token + self.cf_proxies = cf_proxies.rstrip("/") if cf_proxies else None + self.uuid = uuid self.headers = {} self.cookies = {} + self._base_headers = {} # 兼容 StealthSession API + + @property + def _cookies(self): + """兼容 StealthSession 的 _cookies 属性""" + return self.cookies + + @_cookies.setter + def _cookies(self, value): + """兼容 StealthSession 的 _cookies 属性""" + self.cookies = value + + def _update_cookies(self, response): + """ + 从响应中更新 cookies / Update cookies from response + + 支持两种方式: + 1. 从 response.cookies 获取(直接请求时) + 2. 从响应头 Set-Cookie 解析(通过 Workers 代理时) + """ + # 方式1:从 response.cookies 获取 + if hasattr(response, 'cookies'): + try: + for cookie in response.cookies: + if hasattr(cookie, 'name') and hasattr(cookie, 'value'): + self.cookies[cookie.name] = cookie.value + elif isinstance(cookie, str): + if '=' in cookie: + name, value = cookie.split('=', 1) + self.cookies[name.strip()] = value.strip() + except TypeError: + if hasattr(response.cookies, 'items'): + for name, value in response.cookies.items(): + self.cookies[name] = value + + # 方式2:从响应头 Set-Cookie 解析(Workers 代理时需要) + if hasattr(response, 'headers'): + self._parse_set_cookie_headers(response.headers) + + def _parse_set_cookie_headers(self, headers): + """ + 从响应头中解析 Set-Cookie + + Workers 代理会原样返回目标网站的 Set-Cookie 头, + 但 requests 库不会自动解析成 cookies,需要手动处理。 + """ + # 获取所有 Set-Cookie 头 + set_cookie_headers = [] + + # 尝试多种方式获取所有 Set-Cookie 头 + if hasattr(headers, 'get_all'): + # httpx 风格 + set_cookie_headers = headers.get_all('set-cookie') or [] + elif hasattr(headers, 'getlist'): + # urllib3 风格 + set_cookie_headers = headers.getlist('set-cookie') or [] + else: + # requests 风格 - headers 可能合并了多个 Set-Cookie + # 用逗号分隔多个 cookie(但需要小心 Expires 中的逗号) + cookie_header = headers.get('set-cookie', '') + if cookie_header: + # 简单分割,按照 ", " 后跟字母开头的模式 + # 例如: "a=1; Path=/, b=2; Path=/" + import re + # 匹配 ", " 后面紧跟 cookie 名称的模式 + parts = re.split(r',\s*(?=[A-Za-z_][A-Za-z0-9_-]*=)', cookie_header) + set_cookie_headers = [p.strip() for p in parts if p.strip()] + + # 解析每个 Set-Cookie 头 + for cookie_str in set_cookie_headers: + self._parse_single_cookie(cookie_str) + + def _parse_single_cookie(self, cookie_str): + """ + 解析单个 Set-Cookie 字符串 + + 格式示例: + __Host-authjs.csrf-token=xxx%7Cyyy; Path=/; Secure; HttpOnly + """ + if not cookie_str: + return + + # 分割成多个部分 + parts = cookie_str.split(';') + if not parts: + return + + # 第一部分是 name=value + first_part = parts[0].strip() + if '=' not in first_part: + return + + name, value = first_part.split('=', 1) + name = name.strip() + value = value.strip() + + if name: + self.cookies[name] = value def request(self, method, url, **kwargs): """ @@ -94,6 +202,9 @@ class Session: - data (dict/str): 表单数据 / Form data - json (dict): JSON 数据 / JSON data - timeout (int/float): 超时时间(秒) / Timeout (seconds) + - stealth (bool): 启用隐身模式 / Enable stealth mode + - impersonate (str): TLS 指纹模拟 / TLS fingerprint impersonation + - http2 (bool): 启用 HTTP/2 / Enable HTTP/2 - 其他参数与 requests 库兼容 - Other parameters compatible with requests library @@ -105,22 +216,30 @@ class Session: Session-level headers and cookies are automatically added to requests, 但请求级别的参数优先级更高。 but request-level parameters have higher priority. + 响应中的 Set-Cookie 会自动保存到会话中。 + Set-Cookie from response will be automatically saved to session. """ headers = self.headers.copy() + headers.update(self._base_headers) # 应用基础请求头 headers.update(kwargs.pop("headers", {})) cookies = self.cookies.copy() cookies.update(kwargs.pop("cookies", {})) - return request( + response = request( method, url, cf_proxies=self.cf_proxies, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs ) + + # 自动从响应中更新 cookies + self._update_cookies(response) + + return response def get(self, url, **kwargs): """ diff --git a/cfspider/stealth.py b/cfspider/stealth.py index 510ad14..af951b7 100644 --- a/cfspider/stealth.py +++ b/cfspider/stealth.py @@ -220,29 +220,38 @@ def update_sec_fetch_headers(headers: Dict, site_type: str = 'none') -> Dict: class StealthSession: """ - 隐身会话类 + 隐身会话类 / Stealth Session class 提供完整的会话一致性管理,解决反爬虫检测的三大问题: + Provides complete session consistency management, solving three major anti-crawler issues: 1. 固定 User-Agent:整个会话使用同一个浏览器指纹 + Fixed User-Agent: Uses the same browser fingerprint throughout the session 2. 自动管理 Cookie:响应中的 Cookie 自动保存并在后续请求中发送 + Auto Cookie Management: Cookies from responses are saved and sent in subsequent requests 3. 自动添加 Referer:页面跳转时自动添加来源信息 + Auto Referer: Automatically adds origin information during page navigation 4. 随机延迟:每次请求前随机等待,模拟人类行为 + Random Delay: Random wait before each request, simulating human behavior 5. 自动更新 Sec-Fetch-Site:根据 Referer 判断同站/跨站访问 + Auto Sec-Fetch-Site: Updates based on Referer to indicate same-site/cross-site access Attributes: - browser (str): 当前使用的浏览器类型 - cf_proxies (str): 代理地址 - delay (tuple): 随机延迟范围 - auto_referer (bool): 是否自动添加 Referer - last_url (str): 上一次请求的 URL - request_count (int): 会话累计请求次数 + browser (str): 当前使用的浏览器类型 / Current browser type + cf_proxies (str): Workers 代理地址 / Workers proxy address + uuid (str): VLESS UUID(可选,自动获取) / VLESS UUID (optional, auto-fetched) + delay (tuple): 随机延迟范围 / Random delay range + auto_referer (bool): 是否自动添加 Referer / Whether to auto-add Referer + last_url (str): 上一次请求的 URL / Last requested URL + request_count (int): 会话累计请求次数 / Session cumulative request count Example: >>> import cfspider >>> - >>> # 基本用法 - >>> with cfspider.StealthSession(browser='chrome') as session: + >>> # 基本用法(使用 Workers 代理) + >>> with cfspider.StealthSession( + ... cf_proxies="https://cfspider.violetqqcom.workers.dev" + ... ) as session: ... # 第一次请求:Sec-Fetch-Site: none ... r1 = session.get("https://example.com") ... @@ -251,14 +260,17 @@ class StealthSession: ... r2 = session.get("https://example.com/page2") >>> >>> # 带随机延迟 - >>> with cfspider.StealthSession(delay=(1, 3)) as session: + >>> with cfspider.StealthSession( + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", + ... delay=(1, 3) + ... ) as session: ... for url in urls: ... # 每次请求前随机等待 1-3 秒 ... response = session.get(url) >>> - >>> # 结合代理使用 + >>> # 完整配置 >>> with cfspider.StealthSession( - ... cf_proxies="https://your-workers.dev", + ... cf_proxies="https://cfspider.violetqqcom.workers.dev", ... browser='firefox', ... delay=(0.5, 2.0) ... ) as session: @@ -268,59 +280,63 @@ class StealthSession: Note: StealthSession 与普通 Session 的区别: - - Session: 仅保持代理配置和基本请求头 + Differences between StealthSession and regular Session: + - Session: 仅保持代理配置和基本请求头 / Only maintains proxy config and basic headers - StealthSession: 完整的隐身模式,包括浏览器指纹、Cookie 管理、 自动 Referer、随机延迟、Sec-Fetch-* 更新 + Complete stealth mode including browser fingerprint, Cookie management, + auto Referer, random delay, Sec-Fetch-* updates """ def __init__( self, browser: str = 'chrome', cf_proxies: str = None, - cf_workers: bool = True, + uuid: str = None, delay: Tuple[float, float] = None, auto_referer: bool = True, - token: str = None, **kwargs ): """ - 初始化隐身会话 + 初始化隐身会话 / Initialize stealth session Args: browser (str): 浏览器类型,决定使用的 User-Agent 和请求头模板 - - 'chrome': Chrome 131(推荐,最完整的请求头,15 个) - - 'firefox': Firefox 133(含 Sec-GPC 隐私头,12 个) - - 'safari': Safari 18(macOS 风格,5 个) - - 'edge': Edge 131(类似 Chrome,14 个) - - 'chrome_mobile': Chrome Mobile(Android,10 个) - cf_proxies (str, optional): 代理地址 - - 不指定则直接请求目标 URL - - 指定 Workers 地址时配合 cf_workers=True - - 指定普通代理时配合 cf_workers=False - cf_workers (bool): 是否使用 CFspider Workers API(默认 True) + / Browser type, determines User-Agent and header template + - 'chrome': Chrome 131(推荐,最完整的请求头,15 个)/ Recommended, 15 headers + - 'firefox': Firefox 133(含 Sec-GPC 隐私头,12 个)/ Includes privacy headers + - 'safari': Safari 18(macOS 风格,5 个)/ macOS style + - 'edge': Edge 131(类似 Chrome,14 个)/ Similar to Chrome + - 'chrome_mobile': Chrome Mobile(Android,10 个)/ Android mobile + cf_proxies (str, optional): Workers 代理地址 + / Workers proxy address + - 如 "https://cfspider.violetqqcom.workers.dev" + - 不指定则直接请求目标 URL / If not specified, requests directly + - UUID 自动从 Workers 获取 / UUID auto-fetched from Workers + uuid (str, optional): VLESS UUID(可选,不填则自动获取) + / VLESS UUID (optional, auto-fetched if not provided) delay (tuple, optional): 请求间随机延迟范围(秒) + / Random delay range between requests (seconds) - 如 (1, 3) 表示每次请求前随机等待 1-3 秒 - - 第一次请求不会延迟 - - 用于避免请求频率过高被检测 + - e.g., (1, 3) means random wait 1-3 seconds before each request + - 第一次请求不会延迟 / First request won't be delayed auto_referer (bool): 是否自动添加 Referer(默认 True) - - True: 自动使用上一个 URL 作为 Referer - - False: 不自动添加(但可以手动指定) - **kwargs: 保留参数,用于未来扩展 + / Whether to auto-add Referer (default: True) + **kwargs: 保留参数,用于未来扩展 / Reserved for future extensions Example: >>> session = cfspider.StealthSession( ... browser='chrome', - ... cf_proxies='https://your-workers.dev', + ... cf_proxies='https://cfspider.violetqqcom.workers.dev', ... delay=(1, 3), ... auto_referer=True ... ) """ self.browser = browser self.cf_proxies = cf_proxies - self.cf_workers = cf_workers + self.uuid = uuid self.delay = delay self.auto_referer = auto_referer - self.token = token self.last_url = None self.request_count = 0 self._extra_kwargs = kwargs @@ -361,21 +377,75 @@ class StealthSession: random_delay(self.delay[0], self.delay[1]) def _update_cookies(self, response): - """更新 Cookie""" + """ + 从响应中更新 cookies + + 支持两种方式: + 1. 从 response.cookies 获取(直接请求时) + 2. 从响应头 Set-Cookie 解析(通过 Workers 代理时) + """ + # 方式1:从 response.cookies 获取 if hasattr(response, 'cookies'): - for cookie in response.cookies: - self._cookies[cookie.name] = cookie.value + try: + for cookie in response.cookies: + if hasattr(cookie, 'name') and hasattr(cookie, 'value'): + self._cookies[cookie.name] = cookie.value + except TypeError: + if hasattr(response.cookies, 'items'): + for name, value in response.cookies.items(): + self._cookies[name] = value + + # 方式2:从响应头 Set-Cookie 解析(Workers 代理时需要) + if hasattr(response, 'headers'): + self._parse_set_cookie_headers(response.headers) + + def _parse_set_cookie_headers(self, headers): + """从响应头中解析 Set-Cookie""" + set_cookie_headers = [] + + if hasattr(headers, 'get_all'): + set_cookie_headers = headers.get_all('set-cookie') or [] + elif hasattr(headers, 'getlist'): + set_cookie_headers = headers.getlist('set-cookie') or [] + else: + cookie_header = headers.get('set-cookie', '') + if cookie_header: + import re + parts = re.split(r',\s*(?=[A-Za-z_][A-Za-z0-9_-]*=)', cookie_header) + set_cookie_headers = [p.strip() for p in parts if p.strip()] + + for cookie_str in set_cookie_headers: + self._parse_single_cookie(cookie_str) + + def _parse_single_cookie(self, cookie_str): + """解析单个 Set-Cookie 字符串""" + if not cookie_str: + return + parts = cookie_str.split(';') + if not parts: + return + first_part = parts[0].strip() + if '=' not in first_part: + return + name, value = first_part.split('=', 1) + name = name.strip() + value = value.strip() + if name: + self._cookies[name] = value def get(self, url: str, **kwargs) -> Any: """ - 发送 GET 请求 + 发送 GET 请求 / Send GET request Args: - url: 目标 URL - **kwargs: 其他参数 + url (str): 目标 URL / Target URL + **kwargs: 其他参数 / Other parameters + - impersonate (str): TLS 指纹模拟 / TLS fingerprint impersonation + - http2 (bool): 启用 HTTP/2 / Enable HTTP/2 + - 其他参数与 requests 库兼容 / Compatible with requests library Returns: - 响应对象 + CFSpiderResponse: 响应对象 / Response object """ from .api import get as _get @@ -390,8 +460,7 @@ class StealthSession: response = _get( url, cf_proxies=self.cf_proxies, - cf_workers=self.cf_workers, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs @@ -404,7 +473,16 @@ class StealthSession: return response def post(self, url: str, **kwargs) -> Any: - """发送 POST 请求""" + """ + 发送 POST 请求 / Send POST request + + Args: + url (str): 目标 URL / Target URL + **kwargs: 其他参数 / Other parameters + + Returns: + CFSpiderResponse: 响应对象 / Response object + """ from .api import post as _post self._apply_delay() @@ -421,8 +499,7 @@ class StealthSession: response = _post( url, cf_proxies=self.cf_proxies, - cf_workers=self.cf_workers, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs @@ -435,7 +512,7 @@ class StealthSession: return response def put(self, url: str, **kwargs) -> Any: - """发送 PUT 请求""" + """发送 PUT 请求 / Send PUT request""" from .api import put as _put self._apply_delay() @@ -445,8 +522,7 @@ class StealthSession: response = _put( url, cf_proxies=self.cf_proxies, - cf_workers=self.cf_workers, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs @@ -457,7 +533,7 @@ class StealthSession: return response def delete(self, url: str, **kwargs) -> Any: - """发送 DELETE 请求""" + """发送 DELETE 请求 / Send DELETE request""" from .api import delete as _delete self._apply_delay() @@ -467,8 +543,7 @@ class StealthSession: response = _delete( url, cf_proxies=self.cf_proxies, - cf_workers=self.cf_workers, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs @@ -479,7 +554,7 @@ class StealthSession: return response def head(self, url: str, **kwargs) -> Any: - """发送 HEAD 请求""" + """发送 HEAD 请求 / Send HEAD request""" from .api import head as _head self._apply_delay() @@ -489,8 +564,7 @@ class StealthSession: response = _head( url, cf_proxies=self.cf_proxies, - cf_workers=self.cf_workers, - token=self.token, + uuid=self.uuid, headers=headers, cookies=cookies, **kwargs diff --git a/pyproject.toml b/pyproject.toml index f59911b..17d11db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cfspider" -version = "1.8.0" +version = "1.8.2" description = "Cloudflare Workers proxy IP pool client" readme = "README.md" license = {text = "Apache-2.0"} @@ -23,6 +23,16 @@ dependencies = [ "httpx[http2]>=0.25.0", "curl_cffi>=0.5.0", "beautifulsoup4>=4.9.0", + # 浏览器自动化 + "playwright>=1.40.0", + # XPath 数据提取 + "lxml>=4.9.0", + # JSONPath 数据提取 + "jsonpath-ng>=1.5.0", + # Excel 导出 + "openpyxl>=3.0.0", + # 进度条显示 + "tqdm>=4.60.0", ] [project.optional-dependencies] diff --git a/workers.js b/workers.js index 0c3221a..7ab099b 100644 --- a/workers.js +++ b/workers.js @@ -1,314 +1,2168 @@ -// CFspider - Cloudflare Workers 代理 IP 池 v1.8.0 -// 支持:同步/异步请求、TLS指纹模拟、浏览器自动化 - -let 反代IP = ''; -const VERSION = '1.8.0'; -const START_TIME = Date.now(); - +import { connect } from "cloudflare:sockets"; +let config_JSON, 反代IP = '', 启用SOCKS5反代 = null, 启用SOCKS5全局反代 = false, 我的SOCKS5账号 = '', parsedSocks5Address = {}; +let 缓存反代IP, 缓存反代解析数组, 缓存反代数组索引 = 0, 启用反代兜底 = true, ECH_DOH = 'https://doh.cmliussss.net/CMLiussss'; +let SOCKS5白名单 = ['*tapecontent.net', '*cloudatacdn.com', '*loadshare.org', '*cdn-centaurus.com', 'scholar.google.com']; +const Pages静态页面 = 'https://edt-pages.github.io'; +///////////////////////////////////////////////////////主程序入口/////////////////////////////////////////////// export default { async fetch(request, env, ctx) { const url = new URL(request.url); - const path = url.pathname.slice(1).toLowerCase(); - + const UA = request.headers.get('User-Agent') || 'null'; + const upgradeHeader = request.headers.get('Upgrade'); + // CFspider 公开模式:使用固定的公开 UUID,无需配置 + const PUBLIC_UUID = 'cfspider-public-00000000-0000'; // 公开 UUID 前缀 + const 管理员密码 = env.ADMIN || env.admin || env.PASSWORD || env.password || env.pswd || env.TOKEN || env.KEY || env.UUID || env.uuid || 'cfspider-public'; + const 加密秘钥 = env.KEY || 'cfspider-default-key'; + const userIDMD5 = await MD5MD5(管理员密码 + 加密秘钥); + const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; + const envUUID = env.UUID || env.uuid; + const userID = (envUUID && uuidRegex.test(envUUID)) ? envUUID.toLowerCase() : [userIDMD5.slice(0, 8), userIDMD5.slice(8, 12), '4' + userIDMD5.slice(13, 16), '8' + userIDMD5.slice(17, 20), userIDMD5.slice(20)].join('-'); + const hosts = env.HOST ? (await 整理成数组(env.HOST)).map(h => h.toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0]) : [url.hostname]; + const host = hosts[0]; if (env.PROXYIP) { - const proxyIPs = env.PROXYIP.split(',').map(ip => ip.trim()); + const proxyIPs = await 整理成数组(env.PROXYIP); 反代IP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; - } else { - 反代IP = (request.cf?.colo || 'unknown') + '.proxyip.fxxk.dedyn.io'; - } - - const 访问IP = request.headers.get('CF-Connecting-IP') || - request.headers.get('X-Forwarded-For') || - 'UNKNOWN'; - - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*' - }; - - if (request.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - // Token 验证(除了首页、debug 页面和从首页发起的 API 请求) - const referer = request.headers.get('Referer') || ''; - const isFromHomePage = referer && (referer.endsWith('/') || referer.endsWith(url.hostname + '/') || referer.includes(url.hostname + '/?')); - const isPublicApi = (path === 'api/pool' || path === 'api/proxyip') && isFromHomePage; - - if (path !== '' && path !== '/' && path !== 'debug' && !isPublicApi) { - const tokenValidation = validateToken(request, env); - if (!tokenValidation.valid) { - return new Response(JSON.stringify({ - error: 'Unauthorized', - message: 'Invalid or missing token' - }), { - status: 401, - headers: { 'Content-Type': 'application/json', ...corsHeaders } + 启用反代兜底 = false; + } else 反代IP = (request.cf.colo + '.PrOxYIp.CmLiUsSsS.nEt').toLowerCase(); + const 访问IP = request.headers.get('X-Real-IP') || request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For') || request.headers.get('True-Client-IP') || request.headers.get('Fly-Client-IP') || request.headers.get('X-Appengine-Remote-Addr') || request.headers.get('X-Forwarded-For') || request.headers.get('X-Real-IP') || request.headers.get('X-Cluster-Client-IP') || request.cf?.clientTcpRtt || '未知IP'; + if (env.GO2SOCKS5) SOCKS5白名单 = await 整理成数组(env.GO2SOCKS5); + ECH_DOH = env.ECH_DOH || env.DOH || ECH_DOH; + if (!upgradeHeader || upgradeHeader !== 'websocket') { + if (url.protocol === 'http:') return Response.redirect(url.href.replace(`http://${url.hostname}`, `https://${url.hostname}`), 301); + + // CFspider 首页 - 当访问根路径时显示 CFspider 界面 + const cfspiderPath = url.pathname.slice(1).toLowerCase(); + const newIpEnabled = env.NEW_IP !== 'false' && env.NEW_IP !== '0'; + // 检测是否使用默认 UUID(未配置环境变量) + const isDefaultUUID = !envUUID || !uuidRegex.test(envUUID); + if (cfspiderPath === '' || cfspiderPath === '/') { + return new Response(generateCFspiderPage(request, url, 访问IP, userID, newIpEnabled, isDefaultUUID), { + headers: { 'Content-Type': 'text/html; charset=utf-8' } }); } - } - - if (path === '' || path === '/') { - return new Response(generateCyberpunkPage(request, url, 访问IP), { + + // CFspider API 路由 + if (cfspiderPath === 'api/pool') { + return new Response(JSON.stringify(generateIPPool(request)), { + headers: { 'Content-Type': 'application/json' } + }); + } + if (cfspiderPath === 'api/status') { + return new Response(JSON.stringify({ + status: 'online', + version: '1.8.2', + colo: request.cf?.colo || 'unknown', + uptime: Date.now() - (globalThis.START_TIME || Date.now()) + }), { headers: { 'Content-Type': 'application/json' } }); + } + // 返回公开配置(供 cfspider 客户端自动获取) + if (cfspiderPath === 'api/uuid' || cfspiderPath === 'api/config') { + // 从环境变量读取 new_ip 设置,默认为 true + const newIpEnabled = env.NEW_IP !== 'false' && env.NEW_IP !== '0'; + // 如果配置了自定义 UUID,不公开返回(需要用户手动填写) + // 只有使用默认 UUID 时才公开返回 + const configResponse = { + host: url.hostname, + new_ip: newIpEnabled, + version: '1.8.2', + is_default_uuid: isDefaultUUID + }; + // 只有默认 UUID 才公开返回 uuid 字段 + if (isDefaultUUID) { + configResponse.uuid = userID; + configResponse.vless_path = '/' + userID; + } + return new Response(JSON.stringify(configResponse), { headers: { - 'Content-Type': 'text/html; charset=utf-8', - ...corsHeaders + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' } }); } - - if (path === 'debug') { + // 设置 new_ip 开关(通过 POST 请求) + if (cfspiderPath === 'api/config/new_ip' && request.method === 'POST') { + // 注意:Cloudflare Workers 不支持动态修改环境变量 + // 这里只是返回当前状态,实际需要通过 Cloudflare Dashboard 或 wrangler.toml 设置 + const newIpEnabled = env.NEW_IP !== 'false' && env.NEW_IP !== '0'; return new Response(JSON.stringify({ - success: true, - version: VERSION, - proxyip: 反代IP, - cf_info: { - colo: request.cf?.colo || 'unknown', - country: request.cf?.country || 'unknown', - city: request.cf?.city || 'unknown', - region: request.cf?.region || 'unknown', - asn: request.cf?.asn || 'unknown', - timezone: request.cf?.timezone || 'unknown', - latitude: request.cf?.latitude || 'unknown', - longitude: request.cf?.longitude || 'unknown', - postalCode: request.cf?.postalCode || 'unknown', - metroCode: request.cf?.metroCode || 'unknown', - continent: request.cf?.continent || 'unknown' - }, - visitor: { - ip: 访问IP, - country: request.cf?.country || 'unknown', - asn: request.cf?.asn || 'unknown' - }, - request: { - method: request.method, - url: request.url, - headers: Object.fromEntries(request.headers.entries()) - }, - uptime: Math.floor((Date.now() - START_TIME) / 1000) + 's', - timestamp: new Date().toISOString() - }, null, 2), { - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - - if (path === 'api/pool') { - const poolData = await getIPPoolData(request); - return new Response(JSON.stringify(poolData, null, 2), { - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - - if (path === 'api/status') { - return new Response(JSON.stringify({ - success: true, - version: VERSION, - status: 'ONLINE', - uptime: Math.floor((Date.now() - START_TIME) / 1000), - node: { - colo: request.cf?.colo || 'unknown', - country: request.cf?.country || 'unknown', - city: request.cf?.city || 'unknown' - }, - timestamp: new Date().toISOString() - }, null, 2), { - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - - if (path === 'api/fetch') { - const targetUrl = url.searchParams.get('url'); - if (!targetUrl) { - return new Response(JSON.stringify({ error: 'Missing url parameter' }), { - status: 400, - headers: { 'Content-Type': 'application/json', ...corsHeaders } + new_ip: newIpEnabled, + message: '请通过 Cloudflare Dashboard 或 wrangler.toml 设置 NEW_IP 环境变量' + }), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } }); } - try { - const startTime = Date.now(); - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': request.headers.get('User-Agent') || 'CFspider/' + VERSION, - 'Accept': '*/*' + + if (!管理员密码) return fetch(Pages静态页面 + '/noADMIN').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 }); }); + if (env.KV && typeof env.KV.get === 'function') { + const 访问路径 = url.pathname.slice(1).toLowerCase(); + const 区分大小写访问路径 = url.pathname.slice(1); + if (区分大小写访问路径 === 加密秘钥 && 加密秘钥 !== '勿动此默认密钥,有需求请自行通过添加变量KEY进行修改') {//快速订阅 + const params = new URLSearchParams(url.search); + params.set('token', await MD5MD5(host + userID)); + return new Response('重定向中...', { status: 302, headers: { 'Location': `/sub?${params.toString()}` } }); + } else if (访问路径 === 'login') {//处理登录页面和登录请求 + const cookies = request.headers.get('Cookie') || ''; + const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1]; + if (authCookie == await MD5MD5(UA + 加密秘钥 + 管理员密码)) return new Response('重定向中...', { status: 302, headers: { 'Location': '/admin' } }); + if (request.method === 'POST') { + const formData = await request.text(); + const params = new URLSearchParams(formData); + const 输入密码 = params.get('password'); + if (输入密码 === 管理员密码) { + // 密码正确,设置cookie并返回成功标记 + const 响应 = new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + 响应.headers.set('Set-Cookie', `auth=${await MD5MD5(UA + 加密秘钥 + 管理员密码)}; Path=/; Max-Age=86400; HttpOnly`); + return 响应; + } } - }); - const content = await response.text(); - const duration = Date.now() - startTime; - return new Response(content, { - status: response.status, - headers: { - 'Content-Type': response.headers.get('Content-Type') || 'text/plain', - 'X-Proxy-IP': 反代IP, - 'X-CF-Colo': request.cf?.colo || 'unknown', - 'X-Response-Time': duration + 'ms', - ...corsHeaders + return fetch(Pages静态页面 + '/login'); + } else if (访问路径 === 'admin' || 访问路径.startsWith('admin/')) {//验证cookie后响应管理页面 + const cookies = request.headers.get('Cookie') || ''; + const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1]; + // 没有cookie或cookie错误,跳转到/login页面 + if (!authCookie || authCookie !== await MD5MD5(UA + 加密秘钥 + 管理员密码)) return new Response('重定向中...', { status: 302, headers: { 'Location': '/login' } }); + if (访问路径 === 'admin/log.json') {// 读取日志内容 + const 读取日志内容 = await env.KV.get('log.json') || '[]'; + return new Response(读取日志内容, { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } else if (区分大小写访问路径 === 'admin/getCloudflareUsage') {// 查询请求量 + try { + const Usage_JSON = await getCloudflareUsage(url.searchParams.get('Email'), url.searchParams.get('GlobalAPIKey'), url.searchParams.get('AccountID'), url.searchParams.get('APIToken')); + return new Response(JSON.stringify(Usage_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } catch (err) { + const errorResponse = { msg: '查询请求量失败,失败原因:' + err.message, error: err.message }; + return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else if (区分大小写访问路径 === 'admin/getADDAPI') {// 验证优选API + if (url.searchParams.get('url')) { + const 待验证优选URL = url.searchParams.get('url'); + try { + new URL(待验证优选URL); + const 请求优选API内容 = await 请求优选API([待验证优选URL], url.searchParams.get('port') || '443'); + const 优选API的IP = 请求优选API内容[0].length > 0 ? 请求优选API内容[0] : 请求优选API内容[1]; + return new Response(JSON.stringify({ success: true, data: 优选API的IP }, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } catch (err) { + const errorResponse = { msg: '验证优选API失败,失败原因:' + err.message, error: err.message }; + return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } + return new Response(JSON.stringify({ success: false, data: [] }, null, 2), { status: 403, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } else if (访问路径 === 'admin/check') {// SOCKS5代理检查 + let 检测代理响应; + if (url.searchParams.has('socks5')) { + 检测代理响应 = await SOCKS5可用性验证('socks5', url.searchParams.get('socks5')); + } else if (url.searchParams.has('http')) { + 检测代理响应 = await SOCKS5可用性验证('http', url.searchParams.get('http')); + } 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' } }); } - }); + + config_JSON = await 读取config_JSON(env, host, userID, env.PATH); + + if (访问路径 === 'admin/init') {// 重置配置为默认值 + try { + config_JSON = await 读取config_JSON(env, host, userID, env.PATH, true); + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Init_Config', config_JSON)); + config_JSON.init = '配置已重置为默认值'; + return new Response(JSON.stringify(config_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } catch (err) { + const errorResponse = { msg: '配置重置失败,失败原因:' + err.message, error: err.message }; + return new Response(JSON.stringify(errorResponse, null, 2), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else if (request.method === 'POST') {// 处理 KV 操作(POST 请求) + if (访问路径 === 'admin/config.json') { // 保存config.json配置 + try { + const newConfig = await request.json(); + // 验证配置完整性 + if (!newConfig.UUID || !newConfig.HOST) return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + + // 保存到 KV + await env.KV.put('config.json', JSON.stringify(newConfig, null, 2)); + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON)); + return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } catch (error) { + console.error('保存配置失败:', error); + return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else if (访问路径 === 'admin/cf.json') { // 保存cf.json配置 + try { + const newConfig = await request.json(); + const CF_JSON = { Email: null, GlobalAPIKey: null, AccountID: null, APIToken: null, UsageAPI: null }; + if (!newConfig.init || newConfig.init !== true) { + if (newConfig.Email && newConfig.GlobalAPIKey) { + CF_JSON.Email = newConfig.Email; + CF_JSON.GlobalAPIKey = newConfig.GlobalAPIKey; + } else if (newConfig.AccountID && newConfig.APIToken) { + CF_JSON.AccountID = newConfig.AccountID; + CF_JSON.APIToken = newConfig.APIToken; + } else if (newConfig.UsageAPI) { + CF_JSON.UsageAPI = newConfig.UsageAPI; + } else { + return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } + + // 保存到 KV + await env.KV.put('cf.json', JSON.stringify(CF_JSON, null, 2)); + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON)); + return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); } catch (error) { - return new Response(JSON.stringify({ - error: error.message, - proxyip: 反代IP - }), { - status: 502, - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - } - - if (path === 'api/json') { - const targetUrl = url.searchParams.get('url'); - if (!targetUrl) { - return new Response(JSON.stringify({ error: 'Missing url parameter' }), { - status: 400, - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - try { - const startTime = Date.now(); - const response = await fetch(targetUrl, { - headers: { - 'User-Agent': request.headers.get('User-Agent') || 'CFspider/' + VERSION, - 'Accept': 'application/json' + console.error('保存配置失败:', error); + return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else if (访问路径 === 'admin/tg.json') { // 保存tg.json配置 + try { + const newConfig = await request.json(); + if (newConfig.init && newConfig.init === true) { + const TG_JSON = { BotToken: null, ChatID: null }; + await env.KV.put('tg.json', JSON.stringify(TG_JSON, null, 2)); + } else { + if (!newConfig.BotToken || !newConfig.ChatID) return new Response(JSON.stringify({ error: '配置不完整' }), { status: 400, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + await env.KV.put('tg.json', JSON.stringify(newConfig, null, 2)); + } + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Config', config_JSON)); + return new Response(JSON.stringify({ success: true, message: '配置已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } catch (error) { + console.error('保存配置失败:', error); + return new Response(JSON.stringify({ error: '保存配置失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else if (区分大小写访问路径 === 'admin/ADD.txt') { // 保存自定义优选IP + try { + const customIPs = await request.text(); + await env.KV.put('ADD.txt', customIPs);// 保存到 KV + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Save_Custom_IPs', config_JSON)); + return new Response(JSON.stringify({ success: true, message: '自定义IP已保存' }), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } catch (error) { + console.error('保存自定义IP失败:', error); + return new Response(JSON.stringify({ error: '保存自定义IP失败: ' + error.message }), { status: 500, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } + } else return new Response(JSON.stringify({ error: '不支持的POST请求路径' }), { status: 404, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); + } else if (访问路径 === 'admin/config.json') {// 处理 admin/config.json 请求,返回JSON + return new Response(JSON.stringify(config_JSON, null, 2), { status: 200, headers: { 'Content-Type': 'application/json' } }); + } else if (区分大小写访问路径 === 'admin/ADD.txt') {// 处理 admin/ADD.txt 请求,返回本地优选IP + let 本地优选IP = await env.KV.get('ADD.txt') || 'null'; + if (本地优选IP == 'null') 本地优选IP = (await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口))[1]; + return new Response(本地优选IP, { status: 200, headers: { 'Content-Type': 'text/plain;charset=utf-8', 'asn': request.cf.asn } }); + } else if (访问路径 === 'admin/cf.json') {// CF配置文件 + return new Response(JSON.stringify(request.cf, null, 2), { status: 200, headers: { 'Content-Type': 'application/json;charset=utf-8' } }); } - }); - const data = await response.json(); - const duration = Date.now() - startTime; - return new Response(JSON.stringify({ - success: true, - proxyip: 反代IP, - cf_colo: request.cf?.colo || 'unknown', - response_time: duration + 'ms', - data - }, null, 2), { - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); + + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Admin_Login', config_JSON)); + return fetch(Pages静态页面 + '/admin'); + } else if (访问路径 === 'logout' || uuidRegex.test(访问路径)) {//清除cookie并跳转到登录页面 + const 响应 = new Response('重定向中...', { status: 302, headers: { 'Location': '/login' } }); + 响应.headers.set('Set-Cookie', 'auth=; Path=/; Max-Age=0; HttpOnly'); + return 响应; + } else if (访问路径 === 'sub') {//处理订阅请求 + const 订阅TOKEN = await MD5MD5(host + userID); + if (url.searchParams.get('token') === 订阅TOKEN) { + config_JSON = await 读取config_JSON(env, host, userID, env.PATH); + ctx.waitUntil(请求日志记录(env, request, 访问IP, 'Get_SUB', config_JSON)); + const ua = UA.toLowerCase(); + const expire = 4102329600;//2099-12-31 到期时间 + const now = Date.now(); + const today = new Date(now); + today.setHours(0, 0, 0, 0); + const UD = Math.floor(((now - today.getTime()) / 86400000) * 24 * 1099511627776 / 2); + let pagesSum = UD, workersSum = UD, total = 24 * 1099511627776; + if (config_JSON.CF.Usage.success) { + pagesSum = config_JSON.CF.Usage.pages; + workersSum = config_JSON.CF.Usage.workers; + total = Number.isFinite(config_JSON.CF.Usage.max) ? (config_JSON.CF.Usage.max / 1000) * 1024 : 1024 * 100; + } + const responseHeaders = { + "content-type": "text/plain; charset=utf-8", + "Profile-Update-Interval": config_JSON.优选订阅生成.SUBUpdateTime, + "Profile-web-page-url": url.protocol + '//' + url.host + '/admin', + "Subscription-Userinfo": `upload=${pagesSum}; download=${workersSum}; total=${total}; expire=${expire}`, + "Cache-Control": "no-store", + }; + const isSubConverterRequest = url.searchParams.has('b64') || url.searchParams.has('base64') || request.headers.get('subconverter-request') || request.headers.get('subconverter-version') || ua.includes('subconverter') || ua.includes(('CF-Workers-SUB').toLowerCase()); + const 订阅类型 = isSubConverterRequest + ? 'mixed' + : url.searchParams.has('target') + ? url.searchParams.get('target') + : url.searchParams.has('clash') || ua.includes('clash') || ua.includes('meta') || ua.includes('mihomo') + ? 'clash' + : url.searchParams.has('sb') || url.searchParams.has('singbox') || ua.includes('singbox') || ua.includes('sing-box') + ? 'singbox' + : url.searchParams.has('surge') || ua.includes('surge') + ? 'surge&ver=4' + : url.searchParams.has('quanx') || ua.includes('quantumult') + ? 'quanx' + : url.searchParams.has('loon') || ua.includes('loon') + ? 'loon' + : 'mixed'; + + if (!ua.includes('mozilla')) responseHeaders["Content-Disposition"] = `attachment; filename*=utf-8''${encodeURIComponent(config_JSON.优选订阅生成.SUBNAME)}`; + const 协议类型 = (url.searchParams.has('surge') || ua.includes('surge')) ? 'tro' + 'jan' : config_JSON.协议类型; + let 订阅内容 = ''; + if (订阅类型 === 'mixed') { + const 节点路径 = config_JSON.启用0RTT ? config_JSON.PATH + '?ed=2560' : config_JSON.PATH; + const TLS分片参数 = config_JSON.TLS分片 == 'Shadowrocket' ? `&fragment=${encodeURIComponent('1,40-60,30-50,tlshello')}` : config_JSON.TLS分片 == 'Happ' ? `&fragment=${encodeURIComponent('3,1,tlshello')}` : ''; + let 完整优选IP = [], 其他节点LINK = ''; + + if (!url.searchParams.has('sub') && config_JSON.优选订阅生成.local) { // 本地生成订阅 + const 完整优选列表 = config_JSON.优选订阅生成.本地IP库.随机IP ? (await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口))[0] : await env.KV.get('ADD.txt') ? await 整理成数组(await env.KV.get('ADD.txt')) : (await 生成随机IP(request, config_JSON.优选订阅生成.本地IP库.随机数量, config_JSON.优选订阅生成.本地IP库.指定端口))[0]; + const 优选API = [], 优选IP = [], 其他节点 = []; + for (const 元素 of 完整优选列表) { + if (元素.toLowerCase().startsWith('https://')) 优选API.push(元素); + else if (元素.toLowerCase().includes('://')) { + if (元素.includes('#')) { + const 地址备注分离 = 元素.split('#'); + 其他节点.push(地址备注分离[0] + '#' + encodeURIComponent(decodeURIComponent(地址备注分离[1]))); + } else 其他节点.push(元素); + } else 优选IP.push(元素); + } + const 请求优选API内容 = await 请求优选API(优选API); + const 合并其他节点数组 = [...new Set(其他节点.concat(请求优选API内容[1]))]; + 其他节点LINK = 合并其他节点数组.length > 0 ? 合并其他节点数组.join('\n') + '\n' : ''; + const 优选API的IP = 请求优选API内容[0]; + 完整优选IP = [...new Set(优选IP.concat(优选API的IP))]; + } else { // 优选订阅生成器 + let 优选订阅生成器HOST = url.searchParams.get('sub') || config_JSON.优选订阅生成.SUB; + 优选订阅生成器HOST = 优选订阅生成器HOST && !/^https?:\/\//i.test(优选订阅生成器HOST) ? `https://${优选订阅生成器HOST}` : 优选订阅生成器HOST; + const 优选订阅生成器URL = `${优选订阅生成器HOST}/sub?host=example.com&uuid=00000000-0000-4000-8000-000000000000`; + try { + const response = await fetch(优选订阅生成器URL, { headers: { 'User-Agent': 'v2rayN/edge' + 'tunnel (https://github.com/cmliu/edge' + 'tunnel)' } }); + if (!response.ok) return new Response('优选订阅生成器异常:' + response.statusText, { status: response.status }); + const 优选订阅生成器返回订阅内容 = atob(await response.text()); + const 订阅行列表 = 优选订阅生成器返回订阅内容.includes('\r\n') ? 优选订阅生成器返回订阅内容.split('\r\n') : 优选订阅生成器返回订阅内容.split('\n'); + for (const 行内容 of 订阅行列表) { + if (!行内容.trim()) continue; // 跳过空行 + if (行内容.includes('00000000-0000-4000-8000-000000000000') && 行内容.includes('example.com')) { // 这是优选IP行,提取 域名:端口#备注 + const 地址匹配 = 行内容.match(/:\/\/[^@]+@([^?]+)/); + if (地址匹配) { + let 地址端口 = 地址匹配[1], 备注 = ''; // 域名:端口 或 IP:端口 + const 备注匹配 = 行内容.match(/#(.+)$/); + if (备注匹配) 备注 = '#' + decodeURIComponent(备注匹配[1]); + 完整优选IP.push(地址端口 + 备注); + } + } else 其他节点LINK += 行内容 + '\n'; + } } catch (error) { - return new Response(JSON.stringify({ - error: error.message, - proxyip: 反代IP - }), { - status: 502, - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); + return new Response('优选订阅生成器异常:' + error.message, { status: 403 }); + } + } + const ECHLINK参数 = config_JSON.ECH ? `&ech=${encodeURIComponent('cloudflare-ech.com+' + ECH_DOH)}` : ''; + 订阅内容 = 其他节点LINK + 完整优选IP.map(原始地址 => { + // 统一正则: 匹配 域名/IPv4/IPv6地址 + 可选端口 + 可选备注 + // 示例: + // - 域名: hj.xmm1993.top:2096#备注 或 example.com + // - IPv4: 166.0.188.128:443#Los Angeles 或 166.0.188.128 + // - IPv6: [2606:4700::]:443#CMCC 或 [2606:4700::] + const regex = /^(\[[\da-fA-F:]+\]|[\d.]+|[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*)(?::(\d+))?(?:#(.+))?$/; + const match = 原始地址.match(regex); + + let 节点地址, 节点端口 = "443", 节点备注; + + if (match) { + 节点地址 = match[1]; // IP地址或域名(可能带方括号) + 节点端口 = match[2] || "443"; // 端口,默认443 + 节点备注 = match[3] || 节点地址; // 备注,默认为地址本身 + } else { + // 不规范的格式,跳过处理返回null + console.warn(`[订阅内容] 不规范的IP格式已忽略: ${原始地址}`); + return null; + } + + return `${协议类型}://00000000-0000-4000-8000-000000000000@${节点地址}:${节点端口}?security=tls&type=${config_JSON.传输协议 + ECHLINK参数}&host=example.com&fp=${config_JSON.Fingerprint}&sni=example.com&path=${encodeURIComponent(config_JSON.随机路径 ? 随机路径() + 节点路径 : 节点路径) + TLS分片参数}&encryption=none${config_JSON.跳过证书验证 ? '&insecure=1&allowInsecure=1' : ''}#${encodeURIComponent(节点备注)}`; + }).filter(item => item !== null).join('\n'); + } else { // 订阅转换 + const 订阅转换URL = `${config_JSON.订阅转换配置.SUBAPI}/sub?target=${订阅类型}&url=${encodeURIComponent(url.protocol + '//' + url.host + '/sub?target=mixed&token=' + 订阅TOKEN + (url.searchParams.has('sub') && url.searchParams.get('sub') != '' ? `&sub=${url.searchParams.get('sub')}` : ''))}&config=${encodeURIComponent(config_JSON.订阅转换配置.SUBCONFIG)}&emoji=${config_JSON.订阅转换配置.SUBEMOJI}&scv=${config_JSON.跳过证书验证}`; + try { + const response = await fetch(订阅转换URL, { headers: { 'User-Agent': 'Subconverter for ' + 订阅类型 + ' edge' + 'tunnel(https://github.com/cmliu/edge' + 'tunnel)' } }); + if (response.ok) { + 订阅内容 = await response.text(); + if (url.searchParams.has('surge') || ua.includes('surge')) 订阅内容 = Surge订阅配置文件热补丁(订阅内容, url.protocol + '//' + url.host + '/sub?token=' + 订阅TOKEN + '&surge', config_JSON); + } else return new Response('订阅转换后端异常:' + response.statusText, { status: response.status }); + } catch (error) { + return new Response('订阅转换后端异常:' + error.message, { status: 403 }); + } + } + + if (!ua.includes('subconverter')) 订阅内容 = await 批量替换域名(订阅内容.replace(/00000000-0000-4000-8000-000000000000/g, config_JSON.UUID), config_JSON.HOSTS) + + if (订阅类型 === 'mixed' && (!ua.includes('mozilla') || url.searchParams.has('b64') || url.searchParams.has('base64'))) 订阅内容 = btoa(订阅内容); + + if (订阅类型 === 'singbox') { + 订阅内容 = Singbox订阅配置文件热补丁(订阅内容, config_JSON.UUID, config_JSON.Fingerprint, config_JSON.ECH ? await getECH(host) : null); + responseHeaders["content-type"] = 'application/json; charset=utf-8'; + } else if (订阅类型 === 'clash') { + 订阅内容 = Clash订阅配置文件热补丁(订阅内容, config_JSON.UUID, config_JSON.ECH, config_JSON.HOSTS); + responseHeaders["content-type"] = 'application/x-yaml; charset=utf-8'; + } + return new Response(订阅内容, { status: 200, headers: responseHeaders }); + } + } else if (访问路径 === 'locations') {//反代locations列表 + const cookies = request.headers.get('Cookie') || ''; + const authCookie = cookies.split(';').find(c => c.trim().startsWith('auth='))?.split('=')[1]; + if (authCookie && authCookie == await MD5MD5(UA + 加密秘钥 + 管理员密码)) return fetch(new Request('https://speed.cloudflare.com/locations', { headers: { 'Referer': 'https://speed.cloudflare.com/' } })); + } else if (访问路径 === 'robots.txt') return new Response('User-agent: *\nDisallow: /', { status: 200, headers: { 'Content-Type': 'text/plain; charset=UTF-8' } }); + } 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); + } + + let 伪装页URL = env.URL || 'nginx'; + if (伪装页URL && 伪装页URL !== 'nginx' && 伪装页URL !== '1101') { + 伪装页URL = 伪装页URL.trim().replace(/\/$/, ''); + if (!伪装页URL.match(/^https?:\/\//i)) 伪装页URL = 'https://' + 伪装页URL; + if (伪装页URL.toLowerCase().startsWith('http://')) 伪装页URL = 'https://' + 伪装页URL.substring(7); + try { const u = new URL(伪装页URL); 伪装页URL = u.protocol + '//' + u.host; } catch (e) { 伪装页URL = 'nginx'; } + } + if (伪装页URL === '1101') return new Response(await html1101(url.host, 访问IP), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } }); + try { + const 反代URL = new URL(伪装页URL), 新请求头 = new Headers(request.headers); + 新请求头.set('Host', 反代URL.host); + 新请求头.set('Referer', 反代URL.origin); + 新请求头.set('Origin', 反代URL.origin); + if (!新请求头.has('User-Agent') && UA && UA !== 'null') 新请求头.set('User-Agent', UA); + const 反代响应 = await fetch(反代URL.origin + url.pathname + url.search, { method: request.method, headers: 新请求头, body: request.body, cf: request.cf }); + const 内容类型 = 反代响应.headers.get('content-type') || ''; + // 只处理文本类型的响应 + if (/text|javascript|json|xml/.test(内容类型)) { + const 响应内容 = (await 反代响应.text()).replaceAll(反代URL.host, url.host); + return new Response(响应内容, { status: 反代响应.status, headers: { ...Object.fromEntries(反代响应.headers), 'Cache-Control': 'no-store' } }); } - } - - if (path === 'api/proxyip') { - return new Response(JSON.stringify({ - success: true, - proxyip: 反代IP, - colo: request.cf?.colo || 'unknown', - country: request.cf?.country || 'unknown', - city: request.cf?.city || 'unknown' - }, null, 2), { - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - - if (path === 'proxy') { - return handleProxyRequest(request, url, corsHeaders); - } - - return new Response('NOT FOUND', { status: 404 }); + return 反代响应; + } catch (error) { } + return new Response(await nginx(), { status: 200, headers: { 'Content-Type': 'text/html; charset=UTF-8' } }); } }; +///////////////////////////////////////////////////////////////////////WS传输数据/////////////////////////////////////////////// +async function 处理WS请求(request, yourUUID) { + const wssPair = new WebSocketPair(); + const [clientSock, serverSock] = Object.values(wssPair); + serverSock.accept(); + let remoteConnWrapper = { socket: null }; + let isDnsQuery = false; + const earlyData = request.headers.get('sec-websocket-protocol') || ''; + const readable = makeReadableStr(serverSock, earlyData); + let 判断是否是木马 = null; + readable.pipeTo(new WritableStream({ + async write(chunk) { + if (isDnsQuery) return await forwardataudp(chunk, serverSock, null); + if (remoteConnWrapper.socket) { + const writer = remoteConnWrapper.socket.writable.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + return; + } -async function handleProxyRequest(request, url, corsHeaders) { - const targetUrl = url.searchParams.get('url'); - const method = url.searchParams.get('method') || 'GET'; - - if (!targetUrl) { - return new Response(JSON.stringify({ error: 'Missing url parameter' }), { - status: 400, - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } - - const headers = {}; - for (const [key, value] of request.headers.entries()) { - if (key.startsWith('x-cfspider-header-')) { - headers[key.replace('x-cfspider-header-', '')] = value; - } - } - - if (!headers['User-Agent']) { - headers['User-Agent'] = 'CFspider/' + VERSION; - } - - try { - let body = null; - if (method !== 'GET' && method !== 'HEAD') { - body = await request.text(); - } - - const startTime = Date.now(); - const response = await fetch(targetUrl, { - method: method, - headers: headers, - body: body || undefined - }); - const duration = Date.now() - startTime; - - const responseHeaders = new Headers(); - for (const [key, value] of response.headers.entries()) { - responseHeaders.set(key, value); - } - responseHeaders.set('X-CF-Colo', request.cf?.colo || 'unknown'); - responseHeaders.set('X-Response-Time', duration + 'ms'); - responseHeaders.set('Access-Control-Allow-Origin', '*'); - - return new Response(response.body, { - status: response.status, - headers: responseHeaders - }); - } catch (error) { - return new Response(JSON.stringify({ error: error.message }), { - status: 502, - headers: { 'Content-Type': 'application/json', ...corsHeaders } - }); - } + if (判断是否是木马 === null) { + const bytes = new Uint8Array(chunk); + 判断是否是木马 = bytes.byteLength >= 58 && bytes[56] === 0x0d && bytes[57] === 0x0a; + } + + if (remoteConnWrapper.socket) { + const writer = remoteConnWrapper.socket.writable.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + return; + } + + 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); + } else { + const { port, hostname, rawIndex, version, isUDP } = 解析魏烈思请求(chunk, yourUUID); + if (isSpeedTestSite(hostname)) throw new Error('Speedtest site is blocked'); + if (isUDP) { + if (port === 53) isDnsQuery = true; + else throw new Error('UDP is not supported'); + } + 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); + } + }, + })).catch((err) => { + // console.error('Readable pipe error:', err); + }); + + return new Response(null, { status: 101, webSocket: clientSock }); } -async function getIPPoolData(request) { - const colo = request.cf?.colo || 'unknown'; - const country = request.cf?.country || 'unknown'; - const city = request.cf?.city || 'unknown'; - const region = request.cf?.region || 'unknown'; - - const nodeInfo = { - colo: colo, - country: country, - city: city, - region: region, - asn: request.cf?.asn || 'unknown', - timezone: request.cf?.timezone || 'unknown', - latitude: request.cf?.latitude || 'unknown', - longitude: request.cf?.longitude || 'unknown', - continent: request.cf?.continent || 'unknown' - }; - - const ipPool = [ - { ip: `${colo}.edge.cloudflare.com`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 10), region: country, type: 'EDGE' }, - { ip: `172.64.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 10), region: country, type: 'ANYCAST' }, - { ip: `172.67.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 20), region: country, type: 'ANYCAST' }, - { ip: `104.21.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 15), region: country, type: 'ANYCAST' }, - { ip: `162.159.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 25), region: country, type: 'ANYCAST' }, - { ip: `104.24.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, status: 'ONLINE', latency: Math.floor(Math.random() * 50 + 18), region: country, type: 'ANYCAST' }, - ]; - +function 解析木马请求(buffer, passwordPlainText) { + const sha224Password = sha224(passwordPlainText); + if (buffer.byteLength < 56) return { hasError: true, message: "invalid data" }; + let crLfIndex = 56; + if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) return { hasError: true, message: "invalid header format" }; + const password = new TextDecoder().decode(buffer.slice(0, crLfIndex)); + if (password !== sha224Password) return { hasError: true, message: "invalid password" }; + + const socks5DataBuffer = buffer.slice(crLfIndex + 2); + if (socks5DataBuffer.byteLength < 6) return { hasError: true, message: "invalid S5 request data" }; + + const view = new DataView(socks5DataBuffer); + const cmd = view.getUint8(0); + if (cmd !== 1) return { hasError: true, message: "unsupported command, only TCP is allowed" }; + + const atype = view.getUint8(1); + let addressLength = 0; + let addressIndex = 2; + let address = ""; + switch (atype) { + case 1: // IPv4 + addressLength = 4; + address = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)).join("."); + break; + case 3: // Domain + addressLength = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + 1))[0]; + addressIndex += 1; + address = new TextDecoder().decode(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); + break; + case 4: // IPv6 + addressLength = 16; + const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + address = ipv6.join(":"); + break; + default: + return { hasError: true, message: `invalid addressType is ${atype}` }; + } + + if (!address) { + return { hasError: true, message: `address is empty, addressType is ${atype}` }; + } + + const portIndex = addressIndex + addressLength; + const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); + const portRemote = new DataView(portBuffer).getUint16(0); + return { - success: true, - version: VERSION, - timestamp: new Date().toISOString(), - node: nodeInfo, - pool: ipPool, - total: ipPool.length, - online: ipPool.filter(p => p.status === 'ONLINE').length + hasError: false, + addressType: atype, + port: portRemote, + hostname: address, + rawClientData: socks5DataBuffer.slice(portIndex + 4) }; } -function generateCyberpunkPage(request, url, visitorIP) { +function 解析魏烈思请求(chunk, token) { + if (chunk.byteLength < 24) return { hasError: true, message: 'Invalid data' }; + const version = new Uint8Array(chunk.slice(0, 1)); + if (formatIdentifier(new Uint8Array(chunk.slice(1, 17))) !== token) return { hasError: true, message: 'Invalid uuid' }; + const optLen = new Uint8Array(chunk.slice(17, 18))[0]; + const cmd = new Uint8Array(chunk.slice(18 + optLen, 19 + optLen))[0]; + let isUDP = false; + if (cmd === 1) { } else if (cmd === 2) { isUDP = true; } else { return { hasError: true, message: 'Invalid command' }; } + const portIdx = 19 + optLen; + const port = new DataView(chunk.slice(portIdx, portIdx + 2)).getUint16(0); + let addrIdx = portIdx + 2, addrLen = 0, addrValIdx = addrIdx + 1, hostname = ''; + const addressType = new Uint8Array(chunk.slice(addrIdx, addrValIdx))[0]; + switch (addressType) { + case 1: + addrLen = 4; + hostname = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + addrLen)).join('.'); + break; + case 2: + addrLen = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + 1))[0]; + addrValIdx += 1; + hostname = new TextDecoder().decode(chunk.slice(addrValIdx, addrValIdx + addrLen)); + break; + case 3: + addrLen = 16; + const ipv6 = []; + const ipv6View = new DataView(chunk.slice(addrValIdx, addrValIdx + addrLen)); + for (let i = 0; i < 8; i++) ipv6.push(ipv6View.getUint16(i * 2).toString(16)); + hostname = ipv6.join(':'); + break; + default: + return { hasError: true, message: `Invalid address type: ${addressType}` }; + } + 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 connectDirect(address, port, data, 所有反代数组 = null, 反代兜底 = true) { + let remoteSock; + if (所有反代数组 && 所有反代数组.length > 0) { + for (let i = 0; i < 所有反代数组.length; i++) { + const 反代数组索引 = (缓存反代数组索引 + i) % 所有反代数组.length; + const [反代地址, 反代端口] = 所有反代数组[反代数组索引]; + try { + console.log(`[反代连接] 尝试连接到: ${反代地址}:${反代端口} (索引: ${反代数组索引})`); + remoteSock = connect({ hostname: 反代地址, port: 反代端口 }); + // 等待TCP连接真正建立,设置1秒超时 + await Promise.race([ + remoteSock.opened, + new Promise((_, reject) => setTimeout(() => reject(new Error('连接超时')), 1000)) + ]); + const testWriter = remoteSock.writable.getWriter(); + await testWriter.write(data); + testWriter.releaseLock(); + console.log(`[反代连接] 成功连接到: ${反代地址}:${反代端口}`); + 缓存反代数组索引 = 反代数组索引; + return remoteSock; + } catch (err) { + console.log(`[反代连接] 连接失败: ${反代地址}:${反代端口}, 错误: ${err.message}`); + try { remoteSock?.close?.(); } catch (e) { } + continue; + } + } + } + + if (反代兜底) { + remoteSock = connect({ hostname: address, port: port }); + const writer = remoteSock.writable.getWriter(); + await writer.write(data); + writer.releaseLock(); + return remoteSock; + } else { + closeSocketQuietly(ws); + throw new Error('[反代连接] 所有反代连接失败,且未启用反代兜底,连接终止。'); + } + } + + async function connecttoPry() { + let newSocket; + if (启用SOCKS5反代 === 'socks5') { + console.log(`[SOCKS5代理] 代理到: ${host}:${portNum}`); + newSocket = await socks5Connect(host, portNum, rawData); + } else if (启用SOCKS5反代 === 'http' || 启用SOCKS5反代 === 'https') { + console.log(`[HTTP代理] 代理到: ${host}:${portNum}`); + newSocket = await httpConnect(host, portNum, rawData); + } else { + console.log(`[反代连接] 代理到: ${host}:${portNum}`); + const 所有反代数组 = await 解析地址端口(反代IP, host, yourUUID); + newSocket = await connectDirect(atob('UFJPWFlJUC50cDEuMDkwMjI3Lnh5eg=='), 1, rawData, 所有反代数组, 启用反代兜底); + } + remoteConnWrapper.socket = newSocket; + newSocket.closed.catch(() => { }).finally(() => closeSocketQuietly(ws)); + connectStreams(newSocket, ws, respHeader, null); + } + + const 验证SOCKS5白名单 = (addr) => SOCKS5白名单.some(p => new RegExp(`^${p.replace(/\*/g, '.*')}$`, 'i').test(addr)); + if (启用SOCKS5反代 && (启用SOCKS5全局反代 || 验证SOCKS5白名单(host))) { + console.log(`[TCP转发] 启用 SOCKS5/HTTP 全局代理`); + try { + await connecttoPry(); + } catch (err) { + throw err; + } + } else { + try { + console.log(`[TCP转发] 尝试直连到: ${host}:${portNum}`); + const initialSocket = await connectDirect(host, portNum, rawData); + remoteConnWrapper.socket = initialSocket; + connectStreams(initialSocket, ws, respHeader, connecttoPry); + } catch (err) { + await connecttoPry(); + } + } +} + +async function forwardataudp(udpChunk, webSocket, respHeader) { + try { + const tcpSocket = connect({ hostname: '8.8.4.4', port: 53 }); + let vlessHeader = respHeader; + const writer = tcpSocket.writable.getWriter(); + await writer.write(udpChunk); + writer.releaseLock(); + await tcpSocket.readable.pipeTo(new WritableStream({ + async write(chunk) { + if (webSocket.readyState === WebSocket.OPEN) { + if (vlessHeader) { + const response = new Uint8Array(vlessHeader.length + chunk.byteLength); + response.set(vlessHeader, 0); + response.set(chunk, vlessHeader.length); + webSocket.send(response.buffer); + vlessHeader = null; + } else { + webSocket.send(chunk); + } + } + }, + })); + } catch (error) { + // console.error('UDP forward error:', error); + } +} + +function closeSocketQuietly(socket) { + try { + if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CLOSING) { + socket.close(); + } + } catch (error) { } +} + +function formatIdentifier(arr, offset = 0) { + const hex = [...arr.slice(offset, offset + 16)].map(b => b.toString(16).padStart(2, '0')).join(''); + return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`; +} +async function connectStreams(remoteSocket, webSocket, headerData, retryFunc) { + let header = headerData, hasData = false; + await remoteSocket.readable.pipeTo( + new WritableStream({ + async write(chunk, controller) { + hasData = true; + if (webSocket.readyState !== WebSocket.OPEN) controller.error('ws.readyState is not open'); + if (header) { + const response = new Uint8Array(header.length + chunk.byteLength); + response.set(header, 0); + response.set(chunk, header.length); + webSocket.send(response.buffer); + header = null; + } else { + webSocket.send(chunk); + } + }, + abort() { }, + }) + ).catch((err) => { + closeSocketQuietly(webSocket); + }); + if (!hasData && retryFunc) { + await retryFunc(); + } +} + +function makeReadableStr(socket, earlyDataHeader) { + let cancelled = false; + return new ReadableStream({ + start(controller) { + socket.addEventListener('message', (event) => { + if (!cancelled) controller.enqueue(event.data); + }); + socket.addEventListener('close', () => { + if (!cancelled) { + closeSocketQuietly(socket); + controller.close(); + } + }); + socket.addEventListener('error', (err) => controller.error(err)); + const { earlyData, error } = base64ToArray(earlyDataHeader); + if (error) controller.error(error); + else if (earlyData) controller.enqueue(earlyData); + }, + cancel() { + cancelled = true; + closeSocketQuietly(socket); + } + }); +} + +function isSpeedTestSite(hostname) { + const speedTestDomains = [atob('c3BlZWQuY2xvdWRmbGFyZS5jb20=')]; + if (speedTestDomains.includes(hostname)) { + return true; + } + + for (const domain of speedTestDomains) { + if (hostname.endsWith('.' + domain) || hostname === domain) { + return true; + } + } + return false; +} + +function base64ToArray(b64Str) { + if (!b64Str) return { error: null }; + try { + const binaryString = atob(b64Str.replace(/-/g, '+').replace(/_/g, '/')); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return { earlyData: bytes.buffer, error: null }; + } catch (error) { + return { error }; + } +} +////////////////////////////////SOCKS5/HTTP函数/////////////////////////////////////////////// +async function socks5Connect(targetHost, targetPort, initialData) { + const { username, password, hostname, port } = parsedSocks5Address; + const socket = connect({ hostname, port }), writer = socket.writable.getWriter(), reader = socket.readable.getReader(); + try { + const authMethods = username && password ? new Uint8Array([0x05, 0x02, 0x00, 0x02]) : new Uint8Array([0x05, 0x01, 0x00]); + await writer.write(authMethods); + let response = await reader.read(); + if (response.done || response.value.byteLength < 2) throw new Error('S5 method selection failed'); + + const selectedMethod = new Uint8Array(response.value)[1]; + if (selectedMethod === 0x02) { + if (!username || !password) throw new Error('S5 requires authentication'); + const userBytes = new TextEncoder().encode(username), passBytes = new TextEncoder().encode(password); + const authPacket = new Uint8Array([0x01, userBytes.length, ...userBytes, passBytes.length, ...passBytes]); + await writer.write(authPacket); + response = await reader.read(); + if (response.done || new Uint8Array(response.value)[1] !== 0x00) throw new Error('S5 authentication failed'); + } else if (selectedMethod !== 0x00) throw new Error(`S5 unsupported auth method: ${selectedMethod}`); + + const hostBytes = new TextEncoder().encode(targetHost); + const connectPacket = new Uint8Array([0x05, 0x01, 0x00, 0x03, hostBytes.length, ...hostBytes, targetPort >> 8, targetPort & 0xff]); + await writer.write(connectPacket); + response = await reader.read(); + if (response.done || new Uint8Array(response.value)[1] !== 0x00) throw new Error('S5 connection failed'); + + await writer.write(initialData); + 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; + } +} + +async function httpConnect(targetHost, targetPort, initialData) { + const { username, password, hostname, port } = parsedSocks5Address; + const socket = connect({ hostname, port }), writer = socket.writable.getWriter(), reader = socket.readable.getReader(); + try { + const auth = username && password ? `Proxy-Authorization: Basic ${btoa(`${username}:${password}`)}\r\n` : ''; + const request = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}User-Agent: Mozilla/5.0\r\nConnection: keep-alive\r\n\r\n`; + await writer.write(new TextEncoder().encode(request)); + + let responseBuffer = new Uint8Array(0), headerEndIndex = -1, bytesRead = 0; + while (headerEndIndex === -1 && bytesRead < 8192) { + const { done, value } = await reader.read(); + if (done) throw new Error('Connection closed before receiving HTTP response'); + responseBuffer = new Uint8Array([...responseBuffer, ...value]); + bytesRead = responseBuffer.length; + const crlfcrlf = responseBuffer.findIndex((_, i) => i < responseBuffer.length - 3 && responseBuffer[i] === 0x0d && responseBuffer[i + 1] === 0x0a && responseBuffer[i + 2] === 0x0d && responseBuffer[i + 3] === 0x0a); + if (crlfcrlf !== -1) headerEndIndex = crlfcrlf + 4; + } + + if (headerEndIndex === -1) throw new Error('Invalid HTTP response'); + const statusCode = parseInt(new TextDecoder().decode(responseBuffer.slice(0, headerEndIndex)).split('\r\n')[0].match(/HTTP\/\d\.\d\s+(\d+)/)[1]); + if (statusCode < 200 || statusCode >= 300) throw new Error(`Connection failed: HTTP ${statusCode}`); + + await writer.write(initialData); + 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; + } +} +//////////////////////////////////////////////////功能性函数/////////////////////////////////////////////// +function Clash订阅配置文件热补丁(Clash_原始订阅内容, uuid = null, ECH启用 = false, HOSTS = []) { + let clash_yaml = Clash_原始订阅内容.replace(/mode:\s*Rule\b/g, 'mode: rule'); + + // 基础 DNS 配置块(不含 nameserver-policy) + const baseDnsBlock = `dns: + enable: true + default-nameserver: + - 223.5.5.5 + - 119.29.29.29 + - 114.114.114.114 + use-hosts: true + nameserver: + - https://sm2.doh.pub/dns-query + - https://dns.alidns.com/dns-query + fallback: + - 8.8.4.4 + - 101.101.101.101 + - 208.67.220.220 + fallback-filter: + geoip: true + domain: [+.google.com, +.facebook.com, +.youtube.com] + ipcidr: + - 240.0.0.0/4 + - 0.0.0.0/32 + geoip-code: CN +`; + + // 检查是否存在 dns: 字段(可能在任意行,行首无缩进) + const hasDns = /^dns:\s*(?:\n|$)/m.test(clash_yaml); + + // 无论 ECH 是否启用,都确保存在 dns: 配置块 + if (!hasDns) { + clash_yaml = baseDnsBlock + clash_yaml; + } + + // 如果 ECH 启用且 HOSTS 有效,添加 nameserver-policy + if (ECH启用 && HOSTS.length > 0) { + // 生成 HOSTS 的 nameserver-policy 条目 + const hostsEntries = HOSTS.map(host => ` "${host}":\n - tls://8.8.8.8\n - https://doh.cmliussss.com/CMLiussss\n - ${ECH_DOH}`).join('\n'); + + // 检查是否存在 nameserver-policy: + const hasNameserverPolicy = /^\s{2}nameserver-policy:\s*(?:\n|$)/m.test(clash_yaml); + + if (hasNameserverPolicy) { + // 存在 nameserver-policy:,在其后添加 HOSTS 条目 + clash_yaml = clash_yaml.replace( + /^(\s{2}nameserver-policy:\s*\n)/m, + `$1${hostsEntries}\n` + ); + } else { + // 不存在 nameserver-policy:,需要在 dns: 块内添加整个 nameserver-policy + const lines = clash_yaml.split('\n'); + let dnsBlockEndIndex = -1; + let inDnsBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (/^dns:\s*$/.test(line)) { + inDnsBlock = true; + continue; + } + if (inDnsBlock) { + // 检查是否是新的顶级字段(行首无空格且不是空行且不是注释) + if (/^[a-zA-Z]/.test(line)) { + dnsBlockEndIndex = i; + break; + } + } + } + + // 在 dns 块末尾插入 nameserver-policy + const nameserverPolicyBlock = ` nameserver-policy:\n${hostsEntries}`; + if (dnsBlockEndIndex !== -1) { + lines.splice(dnsBlockEndIndex, 0, nameserverPolicyBlock); + } else { + // dns: 是最后一个顶级块,在文件末尾添加 + lines.push(nameserverPolicyBlock); + } + clash_yaml = lines.join('\n'); + } + } + + // 如果没有 uuid 或 ECH 未启用,直接返回 + if (!uuid || !ECH启用) return clash_yaml; + + // ECH 启用时,处理代理节点添加 ech-opts + const lines = clash_yaml.split('\n'); + const processedLines = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // 处理行格式(Flow):- {name: ..., uuid: ..., ...} + if (trimmedLine.startsWith('- {') && (trimmedLine.includes('uuid:') || trimmedLine.includes('password:'))) { + let fullNode = line; + let braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; + + // 如果括号不匹配,继续读取下一行 + while (braceCount > 0 && i + 1 < lines.length) { + i++; + fullNode += '\n' + lines[i]; + braceCount += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length; + } + + // 获取代理类型 + const typeMatch = fullNode.match(/type:\s*(\w+)/); + const proxyType = typeMatch ? typeMatch[1] : 'vless'; + + // 根据代理类型确定要查找的字段 + let credentialField = 'uuid'; + if (proxyType === 'trojan') { + credentialField = 'password'; + } + + // 检查对应字段的值是否匹配 + const credentialPattern = new RegExp(`${credentialField}:\\s*([^,}\\n]+)`); + const credentialMatch = fullNode.match(credentialPattern); + + if (credentialMatch && credentialMatch[1].trim() === uuid.trim()) { + // 在最后一个}前添加ech-opts + fullNode = fullNode.replace(/\}(\s*)$/, `, ech-opts: {enable: true}}$1`); + } + + processedLines.push(fullNode); + i++; + } + // 处理块格式(Block):- name: ..., 后续行为属性 + else if (trimmedLine.startsWith('- name:')) { + // 收集完整的代理节点定义 + let nodeLines = [line]; + let baseIndent = line.search(/\S/); + let topLevelIndent = baseIndent + 2; // 顶级属性的缩进 + i++; + + // 继续读取这个节点的所有属性 + while (i < lines.length) { + const nextLine = lines[i]; + const nextTrimmed = nextLine.trim(); + + // 如果是空行,包含它但不继续 + if (!nextTrimmed) { + nodeLines.push(nextLine); + i++; + break; + } + + const nextIndent = nextLine.search(/\S/); + + // 如果缩进小于等于基础缩进且不是空行,说明节点结束了 + if (nextIndent <= baseIndent && nextTrimmed.startsWith('- ')) { + break; + } + + // 如果缩进更小,节点也结束了 + if (nextIndent < baseIndent && nextTrimmed) { + break; + } + + nodeLines.push(nextLine); + i++; + } + + // 获取代理类型 + const nodeText = nodeLines.join('\n'); + const typeMatch = nodeText.match(/type:\s*(\w+)/); + const proxyType = typeMatch ? typeMatch[1] : 'vless'; + + // 根据代理类型确定要查找的字段 + let credentialField = 'uuid'; + if (proxyType === 'trojan') { + credentialField = 'password'; + } + + // 检查这个节点的对应字段是否匹配 + const credentialPattern = new RegExp(`${credentialField}:\\s*([^\\n]+)`); + const credentialMatch = nodeText.match(credentialPattern); + + if (credentialMatch && credentialMatch[1].trim() === uuid.trim()) { + // 找到在哪里插入ech-opts + // 策略:在最后一个顶级属性后面插入,或在ws-opts之前插入 + let insertIndex = -1; + + for (let j = nodeLines.length - 1; j >= 0; j--) { + // 跳过空行,找到节点中最后一个非空行(可能是顶级属性或其子项) + if (nodeLines[j].trim()) { + insertIndex = j; + break; + } + } + + if (insertIndex >= 0) { + const indent = ' '.repeat(topLevelIndent); + // 在节点末尾(最后一个属性块之后)插入 ech-opts 属性 + nodeLines.splice(insertIndex + 1, 0, + `${indent}ech-opts:`, + `${indent} enable: true` + ); + } + } + + processedLines.push(...nodeLines); + } else { + processedLines.push(line); + i++; + } + } + + return processedLines.join('\n'); +} + +function Singbox订阅配置文件热补丁(sb_json_text, uuid = null, fingerprint = "chrome", ech_config = null) { + try { + let config = JSON.parse(sb_json_text); + + // --- 1. TUN 入站迁移 (1.10.0+) --- + if (Array.isArray(config.inbounds)) { + config.inbounds.forEach(inbound => { + if (inbound.type === 'tun') { + const addresses = []; + if (inbound.inet4_address) addresses.push(inbound.inet4_address); + if (inbound.inet6_address) addresses.push(inbound.inet6_address); + if (addresses.length > 0) { + inbound.address = addresses; + delete inbound.inet4_address; + delete inbound.inet6_address; + } + + const route_addresses = []; + if (Array.isArray(inbound.inet4_route_address)) route_addresses.push(...inbound.inet4_route_address); + if (Array.isArray(inbound.inet6_route_address)) route_addresses.push(...inbound.inet6_route_address); + if (route_addresses.length > 0) { + inbound.route_address = route_addresses; + delete inbound.inet4_route_address; + delete inbound.inet6_route_address; + } + + const route_exclude_addresses = []; + if (Array.isArray(inbound.inet4_route_exclude_address)) route_exclude_addresses.push(...inbound.inet4_route_exclude_address); + if (Array.isArray(inbound.inet6_route_exclude_address)) route_exclude_addresses.push(...inbound.inet6_route_exclude_address); + if (route_exclude_addresses.length > 0) { + inbound.route_exclude_address = route_exclude_addresses; + delete inbound.inet4_route_exclude_address; + delete inbound.inet6_route_exclude_address; + } + } + }); + } + + // --- 2. 迁移 Geosite/GeoIP 到 rule_set (1.8.0+) 及 Actions (1.11.0+) --- + const ruleSetsDefinitions = new Map(); + const processRules = (rules, isDns = false) => { + if (!Array.isArray(rules)) return; + rules.forEach(rule => { + if (rule.geosite) { + const geositeList = Array.isArray(rule.geosite) ? rule.geosite : [rule.geosite]; + rule.rule_set = geositeList.map(name => { + const tag = `geosite-${name}`; + if (!ruleSetsDefinitions.has(tag)) { + ruleSetsDefinitions.set(tag, { + tag: tag, + type: "remote", + format: "binary", + url: `https://gh.090227.xyz/https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-${name}.srs`, + download_detour: "DIRECT" + }); + } + return tag; + }); + delete rule.geosite; + } + if (rule.geoip) { + const geoipList = Array.isArray(rule.geoip) ? rule.geoip : [rule.geoip]; + rule.rule_set = rule.rule_set || []; + geoipList.forEach(name => { + const tag = `geoip-${name}`; + if (!ruleSetsDefinitions.has(tag)) { + ruleSetsDefinitions.set(tag, { + tag: tag, + type: "remote", + format: "binary", + url: `https://gh.090227.xyz/https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-${name}.srs`, + download_detour: "DIRECT" + }); + } + rule.rule_set.push(tag); + }); + delete rule.geoip; + } + const targetField = isDns ? 'server' : 'outbound'; + const actionValue = String(rule[targetField]).toUpperCase(); + if (actionValue === 'REJECT' || actionValue === 'BLOCK') { + rule.action = 'reject'; + rule.method = 'drop'; // 强制使用现代方式 + delete rule[targetField]; + } + }); + }; + + if (config.dns && config.dns.rules) processRules(config.dns.rules, true); + if (config.route && config.route.rules) processRules(config.route.rules, false); + + if (ruleSetsDefinitions.size > 0) { + if (!config.route) config.route = {}; + config.route.rule_set = Array.from(ruleSetsDefinitions.values()); + } + + // --- 3. 兼容性与纠错 --- + if (!config.outbounds) config.outbounds = []; + + // 移除 outbounds 中冗余的 block 类型节点 (如果它们已经被 action 替代) + // 但保留 DIRECT 这种必需的特殊出站 + config.outbounds = config.outbounds.filter(o => { + if (o.tag === 'REJECT' || o.tag === 'block') { + return false; // 移除,因为已经改用 action: reject 了 + } + return true; + }); + + const existingOutboundTags = new Set(config.outbounds.map(o => o.tag)); + + if (!existingOutboundTags.has('DIRECT')) { + config.outbounds.push({ "type": "direct", "tag": "DIRECT" }); + existingOutboundTags.add('DIRECT'); + } + + if (config.dns && config.dns.servers) { + const dnsServerTags = new Set(config.dns.servers.map(s => s.tag)); + if (config.dns.rules) { + config.dns.rules.forEach(rule => { + if (rule.server && !dnsServerTags.has(rule.server)) { + if (rule.server === 'dns_block' && dnsServerTags.has('block')) { + rule.server = 'block'; + } else if (rule.server.toLowerCase().includes('block') && !dnsServerTags.has(rule.server)) { + config.dns.servers.push({ "tag": rule.server, "address": "rcode://success" }); + dnsServerTags.add(rule.server); + } + } + }); + } + } + + config.outbounds.forEach(outbound => { + if (outbound.type === 'selector' || outbound.type === 'urltest') { + if (Array.isArray(outbound.outbounds)) { + // 修正:如果选择器引用了被移除的 REJECT/block,直接将其过滤掉 + // 因为路由规则已经通过 action 拦截了,不需要走选择器 + outbound.outbounds = outbound.outbounds.filter(tag => { + const upperTag = tag.toUpperCase(); + return existingOutboundTags.has(tag) && upperTag !== 'REJECT' && upperTag !== 'BLOCK'; + }); + if (outbound.outbounds.length === 0) outbound.outbounds.push("DIRECT"); + } + } + }); + + // --- 4. UUID 匹配节点的 TLS 热补丁 (utls & ech) --- + if (uuid) { + config.outbounds.forEach(outbound => { + // 仅处理包含 uuid 或 password 且匹配的节点 + if ((outbound.uuid && outbound.uuid === uuid) || (outbound.password && outbound.password === uuid)) { + // 确保 tls 对象存在 + if (!outbound.tls) { + outbound.tls = { enabled: true }; + } + + // 添加/更新 utls 配置 + if (fingerprint) { + outbound.tls.utls = { + enabled: true, + fingerprint: fingerprint + }; + } + + // 如果提供了 ech_config,添加/更新 ech 配置 + if (ech_config) { + outbound.tls.ech = { + enabled: true, + config: `-----BEGIN ECH CONFIGS-----\n${ech_config}\n-----END ECH CONFIGS-----` + }; + } + } + }); + } + + return JSON.stringify(config, null, 2); + } catch (e) { + console.error("Singbox热补丁执行失败:", e); + return JSON.stringify(JSON.parse(sb_json_text), null, 2); + } +} + +function Surge订阅配置文件热补丁(content, url, config_JSON) { + const 每行内容 = content.includes('\r\n') ? content.split('\r\n') : content.split('\n'); + + let 输出内容 = ""; + const realSurgePath = config_JSON.启用0RTT ? config_JSON.PATH + '?ed=2560' : config_JSON.PATH; + for (let x of 每行内容) { + if (x.includes('= tro' + 'jan,') && !x.includes('ws=true') && !x.includes('ws-path=')) { + const host = x.split("sni=")[1].split(",")[0]; + const 备改内容 = `sni=${host}, skip-cert-verify=${config_JSON.跳过证书验证}`; + const 正确内容 = `sni=${host}, skip-cert-verify=${config_JSON.跳过证书验证}, ws=true, ws-path=${realSurgePath}, ws-headers=Host:"${host}"`; + 输出内容 += x.replace(new RegExp(备改内容, 'g'), 正确内容).replace("[", "").replace("]", "") + '\n'; + } else { + 输出内容 += x + '\n'; + } + } + + 输出内容 = `#!MANAGED-CONFIG ${url} interval=${config_JSON.优选订阅生成.SUBUpdateTime * 60 * 60} strict=false` + 输出内容.substring(输出内容.indexOf('\n')); + return 输出内容; +} + +async function 请求日志记录(env, request, 访问IP, 请求类型 = "Get_SUB", config_JSON) { + const KV容量限制 = 4;//MB + try { + const 当前时间 = new Date(); + const 日志内容 = { TYPE: 请求类型, IP: 访问IP, ASN: `AS${request.cf.asn || '0'} ${request.cf.asOrganization || 'Unknown'}`, CC: `${request.cf.country || 'N/A'} ${request.cf.city || 'N/A'}`, URL: request.url, UA: request.headers.get('User-Agent') || 'Unknown', TIME: 当前时间.getTime() }; + let 日志数组 = []; + const 现有日志 = await env.KV.get('log.json'); + if (现有日志) { + try { + 日志数组 = JSON.parse(现有日志); + if (!Array.isArray(日志数组)) { 日志数组 = [日志内容]; } + else if (请求类型 !== "Get_SUB") { + const 三十分钟前时间戳 = 当前时间.getTime() - 30 * 60 * 1000; + if (日志数组.some(log => log.TYPE !== "Get_SUB" && log.IP === 访问IP && log.URL === request.url && log.UA === (request.headers.get('User-Agent') || 'Unknown') && log.TIME >= 三十分钟前时间戳)) return; + 日志数组.push(日志内容); + while (JSON.stringify(日志数组, null, 2).length > KV容量限制 * 1024 * 1024 && 日志数组.length > 0) 日志数组.shift(); + } else { + 日志数组.push(日志内容); + while (JSON.stringify(日志数组, null, 2).length > KV容量限制 * 1024 * 1024 && 日志数组.length > 0) 日志数组.shift(); + } + if (config_JSON.TG.启用) { + try { + const TG_TXT = await env.KV.get('tg.json'); + const TG_JSON = JSON.parse(TG_TXT); + await sendMessage(TG_JSON.BotToken, TG_JSON.ChatID, 日志内容, config_JSON); + } catch (error) { console.error(`读取tg.json出错: ${error.message}`) } + } + } catch (e) { 日志数组 = [日志内容]; } + } else { 日志数组 = [日志内容]; } + await env.KV.put('log.json', JSON.stringify(日志数组, null, 2)); + } catch (error) { console.error(`日志记录失败: ${error.message}`); } +} + +async function sendMessage(BotToken, ChatID, 日志内容, config_JSON) { + if (!BotToken || !ChatID) return; + + try { + const 请求时间 = new Date(日志内容.TIME).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }); + const 请求URL = new URL(日志内容.URL); + const msg = `#${config_JSON.优选订阅生成.SUBNAME} 日志通知\n\n` + + `📌 类型:#${日志内容.TYPE}\n` + + `🌐 IP:${日志内容.IP}\n` + + `📍 位置:${日志内容.CC}\n` + + `🏢 ASN:${日志内容.ASN}\n` + + `🔗 域名:${请求URL.host}\n` + + `🔍 路径:${请求URL.pathname + 请求URL.search}\n` + + `🤖 UA:${日志内容.UA}\n` + + `📅 时间:${请求时间}\n` + + `${config_JSON.CF.Usage.success ? `📊 请求用量:${config_JSON.CF.Usage.total}/100000 ${((config_JSON.CF.Usage.total / 100000) * 100).toFixed(2)}%\n` : ''}`; + + const url = `https://api.telegram.org/bot${BotToken}/sendMessage?chat_id=${ChatID}&parse_mode=HTML&text=${encodeURIComponent(msg)}`; + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;', + 'Accept-Encoding': 'gzip, deflate, br', + 'User-Agent': 日志内容.UA || 'Unknown', + } + }); + } catch (error) { console.error('Error sending message:', error) } +} + +function 掩码敏感信息(文本, 前缀长度 = 3, 后缀长度 = 2) { + if (!文本 || typeof 文本 !== 'string') return 文本; + if (文本.length <= 前缀长度 + 后缀长度) return 文本; // 如果长度太短,直接返回 + + const 前缀 = 文本.slice(0, 前缀长度); + const 后缀 = 文本.slice(-后缀长度); + const 星号数量 = 文本.length - 前缀长度 - 后缀长度; + + return `${前缀}${'*'.repeat(星号数量)}${后缀}`; +} + +async function MD5MD5(文本) { + const 编码器 = new TextEncoder(); + + const 第一次哈希 = await crypto.subtle.digest('MD5', 编码器.encode(文本)); + const 第一次哈希数组 = Array.from(new Uint8Array(第一次哈希)); + const 第一次十六进制 = 第一次哈希数组.map(字节 => 字节.toString(16).padStart(2, '0')).join(''); + + const 第二次哈希 = await crypto.subtle.digest('MD5', 编码器.encode(第一次十六进制.slice(7, 27))); + const 第二次哈希数组 = Array.from(new Uint8Array(第二次哈希)); + const 第二次十六进制 = 第二次哈希数组.map(字节 => 字节.toString(16).padStart(2, '0')).join(''); + + return 第二次十六进制.toLowerCase(); +} + +function 随机路径() { + const 常用路径目录 = ["about", "account", "acg", "act", "activity", "ad", "ads", "ajax", "album", "albums", "anime", "api", "app", "apps", "archive", "archives", "article", "articles", "ask", "auth", "avatar", "bbs", "bd", "blog", "blogs", "book", "books", "bt", "buy", "cart", "category", "categories", "cb", "channel", "channels", "chat", "china", "city", "class", "classify", "clip", "clips", "club", "cn", "code", "collect", "collection", "comic", "comics", "community", "company", "config", "contact", "content", "course", "courses", "cp", "data", "detail", "details", "dh", "directory", "discount", "discuss", "dl", "dload", "doc", "docs", "document", "documents", "doujin", "download", "downloads", "drama", "edu", "en", "ep", "episode", "episodes", "event", "events", "f", "faq", "favorite", "favourites", "favs", "feedback", "file", "files", "film", "films", "forum", "forums", "friend", "friends", "game", "games", "gif", "go", "go.html", "go.php", "group", "groups", "help", "home", "hot", "htm", "html", "image", "images", "img", "index", "info", "intro", "item", "items", "ja", "jp", "jump", "jump.html", "jump.php", "jumping", "knowledge", "lang", "lesson", "lessons", "lib", "library", "link", "links", "list", "live", "lives", "m", "mag", "magnet", "mall", "manhua", "map", "member", "members", "message", "messages", "mobile", "movie", "movies", "music", "my", "new", "news", "note", "novel", "novels", "online", "order", "out", "out.html", "out.php", "outbound", "p", "page", "pages", "pay", "payment", "pdf", "photo", "photos", "pic", "pics", "picture", "pictures", "play", "player", "playlist", "post", "posts", "product", "products", "program", "programs", "project", "qa", "question", "rank", "ranking", "read", "readme", "redirect", "redirect.html", "redirect.php", "reg", "register", "res", "resource", "retrieve", "sale", "search", "season", "seasons", "section", "seller", "series", "service", "services", "setting", "settings", "share", "shop", "show", "shows", "site", "soft", "sort", "source", "special", "star", "stars", "static", "stock", "store", "stream", "streaming", "streams", "student", "study", "tag", "tags", "task", "teacher", "team", "tech", "temp", "test", "thread", "tool", "tools", "topic", "topics", "torrent", "trade", "travel", "tv", "txt", "type", "u", "upload", "uploads", "url", "urls", "user", "users", "v", "version", "video", "videos", "view", "vip", "vod", "watch", "web", "wenku", "wiki", "work", "www", "zh", "zh-cn", "zh-tw", "zip"]; + const 随机数 = Math.floor(Math.random() * 3 + 1); + const 随机路径 = 常用路径目录.sort(() => 0.5 - Math.random()).slice(0, 随机数).join('/'); + return `/${随机路径}`; +} + +function 随机替换通配符(h) { + if (!h?.includes('*')) return h; + const 字符集 = 'abcdefghijklmnopqrstuvwxyz0123456789'; + return h.replace(/\*/g, () => { + let s = ''; + for (let i = 0; i < Math.floor(Math.random() * 14) + 3; i++) + s += 字符集[Math.floor(Math.random() * 36)]; + return s; + }); +} + +function 批量替换域名(内容, hosts, 每组数量 = 2) { + const 打乱后数组 = [...hosts].sort(() => Math.random() - 0.5); + let count = 0, currentRandomHost = null; + return 内容.replace(/example\.com/g, () => { + if (count % 每组数量 === 0) currentRandomHost = 随机替换通配符(打乱后数组[Math.floor(count / 每组数量) % 打乱后数组.length]); + count++; + return currentRandomHost; + }); +} + +async function getECH(host) { + try { + const res = await fetch(`https://1.1.1.1/dns-query?name=${encodeURIComponent(host)}&type=65`, { headers: { 'accept': 'application/dns-json' } }); + const data = await res.json(); + if (!data.Answer?.length) return ''; + for (let ans of data.Answer) { + if (ans.type !== 65 || !ans.data) continue; + const match = ans.data.match(/ech=([^\s]+)/); + if (match) return match[1].replace(/"/g, ''); + if (ans.data.startsWith('\\#')) { + const hex = ans.data.split(' ').slice(2).join(''); + const bytes = new Uint8Array(hex.match(/.{1,2}/g).map(b => parseInt(b, 16))); + let offset = 2; + while (offset < bytes.length && bytes[offset++] !== 0) + offset += bytes[offset - 1]; + + while (offset + 4 <= bytes.length) { + const key = (bytes[offset] << 8) | bytes[offset + 1]; + const len = (bytes[offset + 2] << 8) | bytes[offset + 3]; + offset += 4; + + if (key === 5) return btoa(String.fromCharCode(...bytes.slice(offset, offset + len))); + offset += len; + } + } + } + return ''; + } catch { + return ''; + } +} + +async function 读取config_JSON(env, hostname, userID, path, 重置配置 = false) { + //const host = 随机替换通配符(hostname); + const host = hostname; + const 初始化开始时间 = performance.now(); + const 默认配置JSON = { + TIME: new Date().toISOString(), + HOST: host, + HOSTS: [hostname], + UUID: userID, + 协议类型: "v" + "le" + "ss", + 传输协议: "ws", + 跳过证书验证: true, + 启用0RTT: false, + TLS分片: null, + 随机路径: false, + ECH: false, + Fingerprint: "chrome", + 优选订阅生成: { + local: true, // true: 基于本地的优选地址 false: 优选订阅生成器 + 本地IP库: { + 随机IP: true, // 当 随机IP 为true时生效,启用随机IP的数量,否则使用KV内的ADD.txt + 随机数量: 16, + 指定端口: -1, + }, + SUB: null, + SUBNAME: "edge" + "tunnel", + SUBUpdateTime: 3, // 订阅更新时间(小时) + TOKEN: await MD5MD5(hostname + userID), + }, + 订阅转换配置: { + SUBAPI: "https://SUBAPI.cmliussss.net", + SUBCONFIG: "https://raw.githubusercontent.com/cmliu/ACL4SSR/refs/heads/main/Clash/config/ACL4SSR_Online_Mini_MultiMode_CF.ini", + SUBEMOJI: false, + }, + 反代: { + PROXYIP: "auto", + SOCKS5: { + 启用: 启用SOCKS5反代, + 全局: 启用SOCKS5全局反代, + 账号: 我的SOCKS5账号, + 白名单: SOCKS5白名单, + }, + }, + TG: { + 启用: false, + BotToken: null, + ChatID: null, + }, + CF: { + Email: null, + GlobalAPIKey: null, + AccountID: null, + APIToken: null, + UsageAPI: null, + Usage: { + success: false, + pages: 0, + workers: 0, + total: 0, + max: 100000, + }, + } + }; + + try { + let configJSON = await env.KV.get('config.json'); + if (!configJSON || 重置配置 == true) { + await env.KV.put('config.json', JSON.stringify(默认配置JSON, null, 2)); + config_JSON = 默认配置JSON; + } else { + config_JSON = JSON.parse(configJSON); + } + } catch (error) { + console.error(`读取config_JSON出错: ${error.message}`); + config_JSON = 默认配置JSON; + } + + config_JSON.HOST = host; + if (!config_JSON.HOSTS) config_JSON.HOSTS = [hostname]; + if (env.HOST) config_JSON.HOSTS = (await 整理成数组(env.HOST)).map(h => h.toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0]); + config_JSON.UUID = userID; + config_JSON.PATH = path ? (path.startsWith('/') ? path : '/' + path) : (config_JSON.反代.SOCKS5.启用 ? ('/' + config_JSON.反代.SOCKS5.启用 + (config_JSON.反代.SOCKS5.全局 ? '://' : '=') + config_JSON.反代.SOCKS5.账号) : (config_JSON.反代.PROXYIP === 'auto' ? '/' : `/proxyip=${config_JSON.反代.PROXYIP}`)); + const TLS分片参数 = config_JSON.TLS分片 == 'Shadowrocket' ? `&fragment=${encodeURIComponent('1,40-60,30-50,tlshello')}` : config_JSON.TLS分片 == 'Happ' ? `&fragment=${encodeURIComponent('3,1,tlshello')}` : ''; + if (!config_JSON.Fingerprint) config_JSON.Fingerprint = "chrome"; + if (!config_JSON.ECH) config_JSON.ECH = false; + else config_JSON.优选订阅生成.SUBUpdateTime = 1; // 启用 ECH 时强制将订阅更新时间改为 1 小时 + const ECHLINK参数 = config_JSON.ECH ? `&ech=${encodeURIComponent('cloudflare-ech.com+' + ECH_DOH)}` : ''; + config_JSON.LINK = `${config_JSON.协议类型}://${userID}@${host}:443?security=tls&type=${config_JSON.传输协议 + ECHLINK参数}&host=${host}&fp=${config_JSON.Fingerprint}&sni=${host}&path=${encodeURIComponent(config_JSON.启用0RTT ? config_JSON.PATH + '?ed=2560' : config_JSON.PATH) + TLS分片参数}&encryption=none${config_JSON.跳过证书验证 ? '&insecure=1&allowInsecure=1' : ''}#${encodeURIComponent(config_JSON.优选订阅生成.SUBNAME)}`; + config_JSON.优选订阅生成.TOKEN = await MD5MD5(hostname + userID); + + const 初始化TG_JSON = { BotToken: null, ChatID: null }; + config_JSON.TG = { 启用: config_JSON.TG.启用 ? config_JSON.TG.启用 : false, ...初始化TG_JSON }; + try { + const TG_TXT = await env.KV.get('tg.json'); + if (!TG_TXT) { + await env.KV.put('tg.json', JSON.stringify(初始化TG_JSON, null, 2)); + } else { + const TG_JSON = JSON.parse(TG_TXT); + config_JSON.TG.ChatID = TG_JSON.ChatID ? TG_JSON.ChatID : null; + config_JSON.TG.BotToken = TG_JSON.BotToken ? 掩码敏感信息(TG_JSON.BotToken) : null; + } + } catch (error) { + console.error(`读取tg.json出错: ${error.message}`); + } + + const 初始化CF_JSON = { Email: null, GlobalAPIKey: null, AccountID: null, APIToken: null, UsageAPI: null }; + config_JSON.CF = { ...初始化CF_JSON, Usage: { success: false, pages: 0, workers: 0, total: 0, max: 100000 } }; + try { + const CF_TXT = await env.KV.get('cf.json'); + if (!CF_TXT) { + await env.KV.put('cf.json', JSON.stringify(初始化CF_JSON, null, 2)); + } else { + const CF_JSON = JSON.parse(CF_TXT); + if (CF_JSON.UsageAPI) { + try { + const response = await fetch(CF_JSON.UsageAPI); + const Usage = await response.json(); + config_JSON.CF.Usage = Usage; + } catch (err) { + console.error(`请求 CF_JSON.UsageAPI 失败: ${err.message}`); + } + } else { + config_JSON.CF.Email = CF_JSON.Email ? CF_JSON.Email : null; + config_JSON.CF.GlobalAPIKey = CF_JSON.GlobalAPIKey ? 掩码敏感信息(CF_JSON.GlobalAPIKey) : null; + config_JSON.CF.AccountID = CF_JSON.AccountID ? 掩码敏感信息(CF_JSON.AccountID) : null; + config_JSON.CF.APIToken = CF_JSON.APIToken ? 掩码敏感信息(CF_JSON.APIToken) : null; + config_JSON.CF.UsageAPI = null; + const Usage = await getCloudflareUsage(CF_JSON.Email, CF_JSON.GlobalAPIKey, CF_JSON.AccountID, CF_JSON.APIToken); + config_JSON.CF.Usage = Usage; + } + } + } catch (error) { + console.error(`读取cf.json出错: ${error.message}`); + } + + config_JSON.加载时间 = (performance.now() - 初始化开始时间).toFixed(2) + 'ms'; + return config_JSON; +} + +async function 生成随机IP(request, count = 16, 指定端口 = -1) { + const asnMap = { '9808': 'cmcc', '4837': 'cu', '4134': 'ct' }, asn = request.cf.asn; + const cidr_url = asnMap[asn] ? `https://raw.githubusercontent.com/cmliu/cmliu/main/CF-CIDR/${asnMap[asn]}.txt` : 'https://raw.githubusercontent.com/cmliu/cmliu/main/CF-CIDR.txt'; + const cfname = { '9808': 'CF移动优选', '4837': 'CF联通优选', '4134': 'CF电信优选' }[asn] || 'CF官方优选'; + const cfport = [443, 2053, 2083, 2087, 2096, 8443]; + let cidrList = []; + try { const res = await fetch(cidr_url); cidrList = res.ok ? await 整理成数组(await res.text()) : ['104.16.0.0/13']; } catch { cidrList = ['104.16.0.0/13']; } + + const generateRandomIPFromCIDR = (cidr) => { + const [baseIP, prefixLength] = cidr.split('/'), prefix = parseInt(prefixLength), hostBits = 32 - prefix; + const ipInt = baseIP.split('.').reduce((a, p, i) => a | (parseInt(p) << (24 - i * 8)), 0); + const randomOffset = Math.floor(Math.random() * Math.pow(2, hostBits)); + const mask = (0xFFFFFFFF << hostBits) >>> 0, randomIP = (((ipInt & mask) >>> 0) + randomOffset) >>> 0; + return [(randomIP >>> 24) & 0xFF, (randomIP >>> 16) & 0xFF, (randomIP >>> 8) & 0xFF, randomIP & 0xFF].join('.'); + }; + + const randomIPs = Array.from({ length: count }, () => { + const ip = generateRandomIPFromCIDR(cidrList[Math.floor(Math.random() * cidrList.length)]); + return `${ip}:${指定端口 === -1 ? cfport[Math.floor(Math.random() * cfport.length)] : 指定端口}#${cfname}`; + }); + return [randomIPs, randomIPs.join('\n')]; +} + +async function 整理成数组(内容) { + var 替换后的内容 = 内容.replace(/[ "'\r\n]+/g, ',').replace(/,+/g, ','); + if (替换后的内容.charAt(0) == ',') 替换后的内容 = 替换后的内容.slice(1); + if (替换后的内容.charAt(替换后的内容.length - 1) == ',') 替换后的内容 = 替换后的内容.slice(0, 替换后的内容.length - 1); + const 地址数组 = 替换后的内容.split(','); + return 地址数组; +} + +function isValidBase64(str) { + if (typeof str !== 'string') return false; + const cleanStr = str.replace(/\s/g, ''); + if (cleanStr.length === 0 || cleanStr.length % 4 !== 0) return false; + const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; + if (!base64Regex.test(cleanStr)) return false; + try { + atob(cleanStr); + return true; + } catch { + return false; + } +} + +function base64Decode(str) { + const bytes = new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0))); + const decoder = new TextDecoder('utf-8'); + return decoder.decode(bytes); +} + +async function 请求优选API(urls, 默认端口 = '443', 超时时间 = 3000) { + if (!urls?.length) return [[], [], []]; + const results = new Set(); + let 订阅链接响应的明文LINK内容 = '', 需要订阅转换订阅URLs = []; + await Promise.allSettled(urls.map(async (url) => { + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 超时时间); + const response = await fetch(url, { signal: controller.signal }); + clearTimeout(timeoutId); + let text = ''; + try { + const buffer = await response.arrayBuffer(); + const contentType = (response.headers.get('content-type') || '').toLowerCase(); + const charset = contentType.match(/charset=([^\s;]+)/i)?.[1]?.toLowerCase() || ''; + + // 根据 Content-Type 响应头判断编码优先级 + let decoders = ['utf-8', 'gb2312']; // 默认优先 UTF-8 + if (charset.includes('gb') || charset.includes('gbk') || charset.includes('gb2312')) { + decoders = ['gb2312', 'utf-8']; // 如果明确指定 GB 系编码,优先尝试 GB2312 + } + + // 尝试多种编码解码 + let decodeSuccess = false; + for (const decoder of decoders) { + try { + const decoded = new TextDecoder(decoder).decode(buffer); + // 验证解码结果的有效性 + if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) { + text = decoded; + decodeSuccess = true; + break; + } else if (decoded && decoded.length > 0) { + // 如果有替换字符 (U+FFFD),说明编码不匹配,继续尝试下一个编码 + continue; + } + } catch (e) { + // 该编码解码失败,尝试下一个 + continue; + } + } + + // 如果所有编码都失败或无效,尝试 response.text() + if (!decodeSuccess) { + text = await response.text(); + } + + // 如果返回的是空或无效数据,返回 + if (!text || text.trim().length === 0) { + return; + } + } catch (e) { + console.error('Failed to decode response:', e); + return; + } + + // 预处理订阅内容 + /* + if (text.includes('proxies:') || (text.includes('outbounds"') && text.includes('inbounds"'))) {// Clash Singbox 配置 + 需要订阅转换订阅URLs.add(url); + return; + } + */ + + const 预处理订阅明文内容 = isValidBase64(text) ? base64Decode(text) : text; + if (预处理订阅明文内容.split('#')[0].includes('://')) { + 订阅链接响应的明文LINK内容 += 预处理订阅明文内容 + '\n'; // 追加LINK明文内容 + return; + } + + const lines = text.trim().split('\n').map(l => l.trim()).filter(l => l); + const isCSV = lines.length > 1 && lines[0].includes(','); + const IPV6_PATTERN = /^[^\[\]]*:[^\[\]]*:[^\[\]]/; + if (!isCSV) { + lines.forEach(line => { + const hashIndex = line.indexOf('#'); + const [hostPart, remark] = hashIndex > -1 ? [line.substring(0, hashIndex), line.substring(hashIndex)] : [line, '']; + let hasPort = false; + if (hostPart.startsWith('[')) { + hasPort = /\]:(\d+)$/.test(hostPart); + } else { + const colonIndex = hostPart.lastIndexOf(':'); + hasPort = colonIndex > -1 && /^\d+$/.test(hostPart.substring(colonIndex + 1)); + } + const port = new URL(url).searchParams.get('port') || 默认端口; + results.add(hasPort ? line : `${hostPart}:${port}${remark}`); + }); + } else { + const headers = lines[0].split(',').map(h => h.trim()); + const dataLines = lines.slice(1); + if (headers.includes('IP地址') && headers.includes('端口') && headers.includes('数据中心')) { + const ipIdx = headers.indexOf('IP地址'), portIdx = headers.indexOf('端口'); + const remarkIdx = headers.indexOf('国家') > -1 ? headers.indexOf('国家') : + headers.indexOf('城市') > -1 ? headers.indexOf('城市') : headers.indexOf('数据中心'); + const tlsIdx = headers.indexOf('TLS'); + dataLines.forEach(line => { + const cols = line.split(',').map(c => c.trim()); + if (tlsIdx !== -1 && cols[tlsIdx]?.toLowerCase() !== 'true') return; + const wrappedIP = IPV6_PATTERN.test(cols[ipIdx]) ? `[${cols[ipIdx]}]` : cols[ipIdx]; + results.add(`${wrappedIP}:${cols[portIdx]}#${cols[remarkIdx]}`); + }); + } else if (headers.some(h => h.includes('IP')) && headers.some(h => h.includes('延迟')) && headers.some(h => h.includes('下载速度'))) { + const ipIdx = headers.findIndex(h => h.includes('IP')); + const delayIdx = headers.findIndex(h => h.includes('延迟')); + const speedIdx = headers.findIndex(h => h.includes('下载速度')); + const port = new URL(url).searchParams.get('port') || 默认端口; + dataLines.forEach(line => { + const cols = line.split(',').map(c => c.trim()); + const wrappedIP = IPV6_PATTERN.test(cols[ipIdx]) ? `[${cols[ipIdx]}]` : cols[ipIdx]; + results.add(`${wrappedIP}:${port}#CF优选 ${cols[delayIdx]}ms ${cols[speedIdx]}MB/s`); + }); + } + } + } catch (e) { } + })); + // 将LINK内容转换为数组并去重 + const LINK数组 = 订阅链接响应的明文LINK内容.trim() ? [...new Set(订阅链接响应的明文LINK内容.split(/\r?\n/).filter(line => line.trim() !== ''))] : []; + return [Array.from(results), LINK数组, 需要订阅转换订阅URLs]; +} + +async function 反代参数获取(request) { + const url = new URL(request.url); + const { pathname, searchParams } = url; + const pathLower = pathname.toLowerCase(); + + // 初始化 + 我的SOCKS5账号 = searchParams.get('socks5') || searchParams.get('http') || null; + 启用SOCKS5全局反代 = searchParams.has('globalproxy') || false; + + // 统一处理反代IP参数 (优先级最高,使用正则一次匹配) + const proxyMatch = pathLower.match(/\/(proxyip[.=]|pyip=|ip=)(.+)/); + if (searchParams.has('proxyip')) { + const 路参IP = searchParams.get('proxyip'); + 反代IP = 路参IP.includes(',') ? 路参IP.split(',')[Math.floor(Math.random() * 路参IP.split(',').length)] : 路参IP; + 启用反代兜底 = false; + return; + } else if (proxyMatch) { + const 路参IP = proxyMatch[1] === 'proxyip.' ? `proxyip.${proxyMatch[2]}` : proxyMatch[2]; + 反代IP = 路参IP.includes(',') ? 路参IP.split(',')[Math.floor(Math.random() * 路参IP.split(',').length)] : 路参IP; + 启用反代兜底 = false; + return; + } + + // 处理SOCKS5/HTTP代理参数 + let socksMatch; + if ((socksMatch = pathname.match(/\/(socks5?|http):\/?\/?(.+)/i))) { + // 格式: /socks5://... 或 /http://... + 启用SOCKS5反代 = socksMatch[1].toLowerCase() === 'http' ? 'http' : 'socks5'; + 我的SOCKS5账号 = socksMatch[2].split('#')[0]; + 启用SOCKS5全局反代 = true; + + // 处理Base64编码的用户名密码 + if (我的SOCKS5账号.includes('@')) { + const atIndex = 我的SOCKS5账号.lastIndexOf('@'); + let userPassword = 我的SOCKS5账号.substring(0, atIndex).replaceAll('%3D', '='); + if (/^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i.test(userPassword) && !userPassword.includes(':')) { + userPassword = atob(userPassword); + } + 我的SOCKS5账号 = `${userPassword}@${我的SOCKS5账号.substring(atIndex + 1)}`; + } + } else if ((socksMatch = pathname.match(/\/(g?s5|socks5|g?http)=(.+)/i))) { + // 格式: /socks5=... 或 /s5=... 或 /gs5=... 或 /http=... 或 /ghttp=... + const type = socksMatch[1].toLowerCase(); + 我的SOCKS5账号 = socksMatch[2]; + 启用SOCKS5反代 = type.includes('http') ? 'http' : 'socks5'; + 启用SOCKS5全局反代 = type.startsWith('g') || 启用SOCKS5全局反代; // gs5 或 ghttp 开头启用全局 + } + + // 解析SOCKS5地址 + if (我的SOCKS5账号) { + try { + parsedSocks5Address = await 获取SOCKS5账号(我的SOCKS5账号); + 启用SOCKS5反代 = searchParams.get('http') ? 'http' : 启用SOCKS5反代; + } catch (err) { + console.error('解析SOCKS5地址失败:', err.message); + 启用SOCKS5反代 = null; + } + } else 启用SOCKS5反代 = null; +} + +async function 获取SOCKS5账号(address) { + if (address.includes('@')) { + const lastAtIndex = address.lastIndexOf('@'); + let userPassword = address.substring(0, lastAtIndex).replaceAll('%3D', '='); + const base64Regex = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i; + if (base64Regex.test(userPassword) && !userPassword.includes(':')) userPassword = atob(userPassword); + address = `${userPassword}@${address.substring(lastAtIndex + 1)}`; + } + const atIndex = address.lastIndexOf("@"); + const [hostPart, authPart] = atIndex === -1 ? [address, undefined] : [address.substring(atIndex + 1), address.substring(0, atIndex)]; + + // 解析认证 + let username, password; + if (authPart) { + [username, password] = authPart.split(":"); + if (!password) throw new Error('无效的 SOCKS 地址格式:认证部分必须是 "username:password" 的形式'); + } + + // 解析主机端口 + let hostname, port; + if (hostPart.includes("]:")) { // IPv6带端口 + [hostname, port] = [hostPart.split("]:")[0] + "]", Number(hostPart.split("]:")[1].replace(/[^\d]/g, ''))]; + } else if (hostPart.startsWith("[")) { // IPv6无端口 + [hostname, port] = [hostPart, 80]; + } else { // IPv4/域名 + const parts = hostPart.split(":"); + [hostname, port] = parts.length === 2 ? [parts[0], Number(parts[1].replace(/[^\d]/g, ''))] : [hostPart, 80]; + } + + if (isNaN(port)) throw new Error('无效的 SOCKS 地址格式:端口号必须是数字'); + if (hostname.includes(":") && !/^\[.*\]$/.test(hostname)) throw new Error('无效的 SOCKS 地址格式:IPv6 地址必须用方括号括起来,如 [2001:db8::1]'); + + return { username, password, hostname, port }; +} + +async function getCloudflareUsage(Email, GlobalAPIKey, AccountID, APIToken) { + const API = "https://api.cloudflare.com/client/v4"; + const sum = (a) => a?.reduce((t, i) => t + (i?.sum?.requests || 0), 0) || 0; + const cfg = { "Content-Type": "application/json" }; + + try { + if (!AccountID && (!Email || !GlobalAPIKey)) return { success: false, pages: 0, workers: 0, total: 0, max: 100000 }; + + if (!AccountID) { + const r = await fetch(`${API}/accounts`, { + method: "GET", + headers: { ...cfg, "X-AUTH-EMAIL": Email, "X-AUTH-KEY": GlobalAPIKey } + }); + if (!r.ok) throw new Error(`账户获取失败: ${r.status}`); + const d = await r.json(); + if (!d?.result?.length) throw new Error("未找到账户"); + const idx = d.result.findIndex(a => a.name?.toLowerCase().startsWith(Email.toLowerCase())); + AccountID = d.result[idx >= 0 ? idx : 0]?.id; + } + + const now = new Date(); + now.setUTCHours(0, 0, 0, 0); + const hdr = APIToken ? { ...cfg, "Authorization": `Bearer ${APIToken}` } : { ...cfg, "X-AUTH-EMAIL": Email, "X-AUTH-KEY": GlobalAPIKey }; + + const res = await fetch(`${API}/graphql`, { + method: "POST", + headers: hdr, + body: JSON.stringify({ + query: `query getBillingMetrics($AccountID: String!, $filter: AccountWorkersInvocationsAdaptiveFilter_InputObject) { + viewer { accounts(filter: {accountTag: $AccountID}) { + pagesFunctionsInvocationsAdaptiveGroups(limit: 1000, filter: $filter) { sum { requests } } + workersInvocationsAdaptive(limit: 10000, filter: $filter) { sum { requests } } + } } + }`, + variables: { AccountID, filter: { datetime_geq: now.toISOString(), datetime_leq: new Date().toISOString() } } + }) + }); + + if (!res.ok) throw new Error(`查询失败: ${res.status}`); + const result = await res.json(); + if (result.errors?.length) throw new Error(result.errors[0].message); + + const acc = result?.data?.viewer?.accounts?.[0]; + if (!acc) throw new Error("未找到账户数据"); + + const pages = sum(acc.pagesFunctionsInvocationsAdaptiveGroups); + const workers = sum(acc.workersInvocationsAdaptive); + const total = pages + workers; + const max = 100000; + console.log(`统计结果 - Pages: ${pages}, Workers: ${workers}, 总计: ${total}, 上限: 100000`); + return { success: true, pages, workers, total, max }; + + } catch (error) { + console.error('获取使用量错误:', error.message); + return { success: false, pages: 0, workers: 0, total: 0, max: 100000 }; + } +} + +function sha224(s) { + const K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]; + const r = (n, b) => ((n >>> b) | (n << (32 - b))) >>> 0; + s = unescape(encodeURIComponent(s)); + const l = s.length * 8; s += String.fromCharCode(0x80); + while ((s.length * 8) % 512 !== 448) s += String.fromCharCode(0); + const h = [0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939, 0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4]; + const hi = Math.floor(l / 0x100000000), lo = l & 0xFFFFFFFF; + s += String.fromCharCode((hi >>> 24) & 0xFF, (hi >>> 16) & 0xFF, (hi >>> 8) & 0xFF, hi & 0xFF, (lo >>> 24) & 0xFF, (lo >>> 16) & 0xFF, (lo >>> 8) & 0xFF, lo & 0xFF); + const w = []; for (let i = 0; i < s.length; i += 4)w.push((s.charCodeAt(i) << 24) | (s.charCodeAt(i + 1) << 16) | (s.charCodeAt(i + 2) << 8) | s.charCodeAt(i + 3)); + for (let i = 0; i < w.length; i += 16) { + const x = new Array(64).fill(0); + for (let j = 0; j < 16; j++)x[j] = w[i + j]; + for (let j = 16; j < 64; j++) { + const s0 = r(x[j - 15], 7) ^ r(x[j - 15], 18) ^ (x[j - 15] >>> 3); + const s1 = r(x[j - 2], 17) ^ r(x[j - 2], 19) ^ (x[j - 2] >>> 10); + x[j] = (x[j - 16] + s0 + x[j - 7] + s1) >>> 0; + } + let [a, b, c, d, e, f, g, h0] = h; + for (let j = 0; j < 64; j++) { + const S1 = r(e, 6) ^ r(e, 11) ^ r(e, 25), ch = (e & f) ^ (~e & g), t1 = (h0 + S1 + ch + K[j] + x[j]) >>> 0; + const S0 = r(a, 2) ^ r(a, 13) ^ r(a, 22), maj = (a & b) ^ (a & c) ^ (b & c), t2 = (S0 + maj) >>> 0; + h0 = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0; + } + for (let j = 0; j < 8; j++)h[j] = (h[j] + (j === 0 ? a : j === 1 ? b : j === 2 ? c : j === 3 ? d : j === 4 ? e : j === 5 ? f : j === 6 ? g : h0)) >>> 0; + } + let hex = ''; + for (let i = 0; i < 7; i++) { + for (let j = 24; j >= 0; j -= 8)hex += ((h[i] >>> j) & 0xFF).toString(16).padStart(2, '0'); + } + return hex; +} + +async function 解析地址端口(proxyIP, 目标域名 = 'dash.cloudflare.com', UUID = '00000000-0000-4000-8000-000000000000') { + if (!缓存反代IP || !缓存反代解析数组 || 缓存反代IP !== proxyIP) { + proxyIP = proxyIP.toLowerCase(); + async function DoH查询(域名, 记录类型) { + try { + const response = await fetch(`https://1.1.1.1/dns-query?name=${域名}&type=${记录类型}`, { + headers: { 'Accept': 'application/dns-json' } + }); + if (!response.ok) return []; + const data = await response.json(); + return data.Answer || []; + } catch (error) { + console.error(`DoH查询失败 (${记录类型}):`, error); + return []; + } + } + + function 解析地址端口字符串(str) { + let 地址 = str, 端口 = 443; + if (str.includes(']:')) { + const parts = str.split(']:'); + 地址 = parts[0] + ']'; + 端口 = parseInt(parts[1], 10) || 端口; + } else if (str.includes(':') && !str.startsWith('[')) { + const colonIndex = str.lastIndexOf(':'); + 地址 = str.slice(0, colonIndex); + 端口 = parseInt(str.slice(colonIndex + 1), 10) || 端口; + } + return [地址, 端口]; + } + + let 所有反代数组 = []; + + if (proxyIP.includes('.william')) { + try { + const txtRecords = await DoH查询(proxyIP, 'TXT'); + const txtData = txtRecords.filter(r => r.type === 16).map(r => r.data); + if (txtData.length > 0) { + let data = txtData[0]; + if (data.startsWith('"') && data.endsWith('"')) data = data.slice(1, -1); + const prefixes = data.replace(/\\010/g, ',').replace(/\n/g, ',').split(',').map(s => s.trim()).filter(Boolean); + 所有反代数组 = prefixes.map(prefix => 解析地址端口字符串(prefix)); + } + } catch (error) { + console.error('解析William域名失败:', error); + } + } else { + let [地址, 端口] = 解析地址端口字符串(proxyIP); + + if (proxyIP.includes('.tp')) { + const tpMatch = proxyIP.match(/\.tp(\d+)/); + if (tpMatch) 端口 = parseInt(tpMatch[1], 10); + } + + // 判断是否是域名(非IP地址) + const ipv4Regex = /^(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)\.(25[0-5]|2[0-4]\d|[01]?\d\d?)$/; + const ipv6Regex = /^\[?([a-fA-F0-9:]+)\]?$/; + + if (!ipv4Regex.test(地址) && !ipv6Regex.test(地址)) { + // 并行查询 A 和 AAAA 记录 + const [aRecords, aaaaRecords] = await Promise.all([ + DoH查询(地址, 'A'), + DoH查询(地址, 'AAAA') + ]); + + const ipv4List = aRecords.filter(r => r.type === 1).map(r => r.data); + const ipv6List = aaaaRecords.filter(r => r.type === 28).map(r => `[${r.data}]`); + const ipAddresses = [...ipv4List, ...ipv6List]; + + 所有反代数组 = ipAddresses.length > 0 + ? ipAddresses.map(ip => [ip, 端口]) + : [[地址, 端口]]; + } else { + 所有反代数组 = [[地址, 端口]]; + } + } + const 排序后数组 = 所有反代数组.sort((a, b) => a[0].localeCompare(b[0])); + const 目标根域名 = 目标域名.includes('.') ? 目标域名.split('.').slice(-2).join('.') : 目标域名; + let 随机种子 = [...(目标根域名 + UUID)].reduce((a, c) => a + c.charCodeAt(0), 0); + console.log(`[反代解析] 随机种子: ${随机种子}\n目标站点: ${目标根域名}`) + const 洗牌后 = [...排序后数组].sort(() => (随机种子 = (随机种子 * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff - 0.5); + 缓存反代解析数组 = 洗牌后.slice(0, 8); + console.log(`[反代解析] 解析完成 总数: ${缓存反代解析数组.length}个\n${缓存反代解析数组.map(([ip, port], index) => `${index + 1}. ${ip}:${port}`).join('\n')}`); + 缓存反代IP = proxyIP; + } else console.log(`[反代解析] 读取缓存 总数: ${缓存反代解析数组.length}个\n${缓存反代解析数组.map(([ip, port], index) => `${index + 1}. ${ip}:${port}`).join('\n')}`); + return 缓存反代解析数组; +} + +async function SOCKS5可用性验证(代理协议 = 'socks5', 代理参数) { + const startTime = Date.now(); + try { parsedSocks5Address = await 获取SOCKS5账号(代理参数); } catch (err) { return { success: false, error: err.message, proxy: 代理协议 + "://" + 代理参数, responseTime: Date.now() - startTime }; } + const { username, password, hostname, port } = parsedSocks5Address; + const 完整代理参数 = username && password ? `${username}:${password}@${hostname}:${port}` : `${hostname}:${port}`; + try { + const initialData = new Uint8Array(0); + const tcpSocket = 代理协议 == 'socks5' ? await socks5Connect('check.socks5.090227.xyz', 80, initialData) : await httpConnect('check.socks5.090227.xyz', 80, initialData); + if (!tcpSocket) return { success: false, error: '无法连接到代理服务器', proxy: 代理协议 + "://" + 完整代理参数, responseTime: Date.now() - startTime }; + try { + const writer = tcpSocket.writable.getWriter(), encoder = new TextEncoder(); + await writer.write(encoder.encode(`GET /cdn-cgi/trace HTTP/1.1\r\nHost: check.socks5.090227.xyz\r\nConnection: close\r\n\r\n`)); + writer.releaseLock(); + const reader = tcpSocket.readable.getReader(), decoder = new TextDecoder(); + let response = ''; + try { while (true) { const { done, value } = await reader.read(); if (done) break; response += decoder.decode(value, { stream: true }); } } finally { reader.releaseLock(); } + await tcpSocket.close(); + return { success: true, proxy: 代理协议 + "://" + 完整代理参数, ip: response.match(/ip=(.*)/)[1], loc: response.match(/loc=(.*)/)[1], responseTime: Date.now() - startTime }; + } catch (error) { + try { await tcpSocket.close(); } catch (e) { console.log('关闭连接时出错:', e); } + return { success: false, error: error.message, proxy: 代理协议 + "://" + 完整代理参数, responseTime: Date.now() - startTime }; + } + } catch (error) { return { success: false, error: error.message, proxy: 代理协议 + "://" + 完整代理参数, responseTime: Date.now() - startTime }; } +} +//////////////////////////////////////////////////////HTML伪装页面/////////////////////////////////////////////// +async function nginx() { + return ` + + + + Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and + working. Further configuration is required.

+ +

For online documentation and support please refer to + nginx.org.
+ Commercial support is available at + nginx.com.

+ +

Thank you for using nginx.

+ + + ` +} + +async function html1101(host, 访问IP) { + const now = new Date(); + const 格式化时间戳 = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0'); + const 随机字符串 = Array.from(crypto.getRandomValues(new Uint8Array(8))).map(b => b.toString(16).padStart(2, '0')).join(''); + + return ` + + + + + +Worker threw exception | ${host} | Cloudflare + + + + + + + + + + + + + + + + +
+ +
+
+

+ Error + 1101 + Ray ID: ${随机字符串} • ${格式化时间戳} UTC +

+

Worker threw exception

+
+ +
+ +
+
+
+

What happened?

+

You've requested a page on a website (${host}) that is on the Cloudflare network. An unknown error occurred while rendering the page.

+
+ +
+

What can I do?

+

If you are the owner of this website:
refer to Workers - Errors and Exceptions and check Workers Logs for ${host}.

+
+ +
+
+ + + +
+
+ + + +`; +} + +// ==================== CFspider 界面函数 ==================== +globalThis.START_TIME = globalThis.START_TIME || Date.now(); + +function generateIPPool(request) { + const colo = request.cf?.colo || 'UNKNOWN'; + const country = request.cf?.country || 'XX'; + const ipPool = [ + { ip: `${colo}.edge.cloudflare.com`, type: 'EDGE', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 10, region: country }, + { ip: `172.64.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, type: 'ANYCAST', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 20, region: country }, + { ip: `172.67.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, type: 'ANYCAST', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 30, region: country }, + { ip: `104.21.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, type: 'ANYCAST', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 25, region: country }, + { ip: `162.159.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, type: 'ANYCAST', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 35, region: country }, + { ip: `104.24.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`, type: 'ANYCAST', status: 'ONLINE', latency: Math.floor(Math.random() * 50) + 28, region: country } + ]; + return { pool: ipPool, total: ipPool.length, online: ipPool.length }; +} + +function generateCFspiderPage(request, url, visitorIP, userID, newIpEnabled = true, isDefaultUUID = false) { const colo = request.cf?.colo || 'UNKNOWN'; const country = request.cf?.country || 'XX'; const city = request.cf?.city || 'Night City'; @@ -319,1066 +2173,256 @@ function generateCyberpunkPage(request, url, visitorIP) { const longitude = request.cf?.longitude || '0'; const continent = request.cf?.continent || 'XX'; const lang = url.searchParams.get('lang') || 'zh'; + const VERSION = '1.8.2'; + + // 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}` : ''; const countryNames = { 'JP': '日本', 'CN': '中国', 'US': '美国', 'HK': '香港', 'TW': '台湾', - 'SG': '新加坡', 'KR': '韩国', 'DE': '德国', 'FR': '法国', 'GB': '英国', - 'AU': '澳大利亚', 'CA': '加拿大', 'NL': '荷兰', 'IN': '印度', 'RU': '俄罗斯', - 'BR': '巴西', 'ID': '印度尼西亚', 'TH': '泰国', 'VN': '越南', 'MY': '马来西亚', - 'PH': '菲律宾', 'IT': '意大利', 'ES': '西班牙', 'MX': '墨西哥', 'AE': '阿联酋' - }; - - const cityNames = { - // 国际城市 - 'Tokyo': '东京', 'Osaka': '大阪', 'Nagoya': '名古屋', 'Fukuoka': '福冈', 'Sapporo': '札幌', - 'Singapore': '新加坡', 'Seoul': '首尔', 'Busan': '釜山', 'Taipei': '台北', 'Kaohsiung': '高雄', - 'London': '伦敦', 'Paris': '巴黎', 'Frankfurt': '法兰克福', 'Amsterdam': '阿姆斯特丹', 'Berlin': '柏林', - 'New York': '纽约', 'Los Angeles': '洛杉矶', 'San Francisco': '旧金山', 'Chicago': '芝加哥', 'Seattle': '西雅图', - 'Sydney': '悉尼', 'Melbourne': '墨尔本', 'Toronto': '多伦多', 'Vancouver': '温哥华', - 'Mumbai': '孟买', 'New Delhi': '新德里', 'Dubai': '迪拜', 'Bangkok': '曼谷', - 'Kuala Lumpur': '吉隆坡', 'Jakarta': '雅加达', 'Manila': '马尼拉', 'Ho Chi Minh City': '胡志明市', 'Hanoi': '河内', - 'Moscow': '莫斯科', 'Madrid': '马德里', 'Rome': '罗马', 'Milan': '米兰', 'Vienna': '维也纳', - // 中国城市 - 直辖市 - 'Beijing': '北京', 'Shanghai': '上海', 'Tianjin': '天津', 'Chongqing': '重庆', - // 中国城市 - 特别行政区 - 'Hong Kong': '香港', 'Macau': '澳门', 'Macao': '澳门', - // 中国城市 - 省会及主要城市 - 'Guangzhou': '广州', 'Shenzhen': '深圳', 'Dongguan': '东莞', 'Foshan': '佛山', 'Zhuhai': '珠海', 'Zhongshan': '中山', 'Huizhou': '惠州', 'Jiangmen': '江门', 'Shantou': '汕头', 'Zhanjiang': '湛江', - 'Nanning': '南宁', 'Guilin': '桂林', 'Liuzhou': '柳州', 'Beihai': '北海', - 'Chengdu': '成都', 'Mianyang': '绵阳', 'Leshan': '乐山', 'Yibin': '宜宾', - 'Kunming': '昆明', 'Dali': '大理', 'Lijiang': '丽江', - 'Guiyang': '贵阳', 'Zunyi': '遵义', - 'Hangzhou': '杭州', 'Ningbo': '宁波', 'Wenzhou': '温州', 'Jiaxing': '嘉兴', 'Shaoxing': '绍兴', 'Jinhua': '金华', - 'Nanjing': '南京', 'Suzhou': '苏州', 'Wuxi': '无锡', 'Changzhou': '常州', 'Nantong': '南通', 'Xuzhou': '徐州', 'Yangzhou': '扬州', 'Zhenjiang': '镇江', - 'Hefei': '合肥', 'Wuhu': '芜湖', 'Bengbu': '蚌埠', 'Anqing': '安庆', - 'Wuhan': '武汉', 'Yichang': '宜昌', 'Xiangyang': '襄阳', 'Jingzhou': '荆州', - 'Changsha': '长沙', 'Zhuzhou': '株洲', 'Xiangtan': '湘潭', 'Hengyang': '衡阳', - 'Nanchang': '南昌', 'Jiujiang': '九江', 'Ganzhou': '赣州', - 'Fuzhou': '福州', 'Xiamen': '厦门', 'Quanzhou': '泉州', 'Zhangzhou': '漳州', 'Putian': '莆田', - 'Jinan': '济南', 'Qingdao': '青岛', 'Yantai': '烟台', 'Weifang': '潍坊', 'Zibo': '淄博', 'Linyi': '临沂', 'Weihai': '威海', - 'Zhengzhou': '郑州', 'Luoyang': '洛阳', 'Kaifeng': '开封', 'Xinxiang': '新乡', 'Nanyang': '南阳', - 'Shijiazhuang': '石家庄', 'Tangshan': '唐山', 'Baoding': '保定', 'Langfang': '廊坊', 'Handan': '邯郸', 'Qinhuangdao': '秦皇岛', - 'Taiyuan': '太原', 'Datong': '大同', 'Linfen': '临汾', - 'Xian': '西安', "Xi'an": '西安', 'Xianyang': '咸阳', 'Baoji': '宝鸡', 'Weinan': '渭南', - 'Lanzhou': '兰州', 'Tianshui': '天水', - 'Xining': '西宁', - 'Yinchuan': '银川', - 'Shenyang': '沈阳', 'Dalian': '大连', 'Anshan': '鞍山', 'Fushun': '抚顺', 'Jinzhou': '锦州', - 'Changchun': '长春', 'Jilin City': '吉林市', 'Siping': '四平', - 'Harbin': '哈尔滨', 'Daqing': '大庆', 'Qiqihar': '齐齐哈尔', 'Mudanjiang': '牡丹江', - 'Hohhot': '呼和浩特', 'Baotou': '包头', 'Ordos': '鄂尔多斯', - 'Urumqi': '乌鲁木齐', 'Kashgar': '喀什', 'Korla': '库尔勒', - 'Lhasa': '拉萨', 'Shigatse': '日喀则', - 'Haikou': '海口', 'Sanya': '三亚', - // 中国省份/地区 - 'Guangdong': '广东', 'Guangxi': '广西', 'Sichuan': '四川', 'Yunnan': '云南', 'Guizhou': '贵州', - 'Zhejiang': '浙江', 'Jiangsu': '江苏', 'Anhui': '安徽', 'Hubei': '湖北', 'Hunan': '湖南', - 'Jiangxi': '江西', 'Fujian': '福建', 'Shandong': '山东', 'Henan': '河南', 'Hebei': '河北', - 'Shanxi': '山西', 'Shaanxi': '陕西', 'Gansu': '甘肃', 'Qinghai': '青海', 'Ningxia': '宁夏', - 'Liaoning': '辽宁', 'Jilin': '吉林', 'Heilongjiang': '黑龙江', - 'Inner Mongolia': '内蒙古', 'Xinjiang': '新疆', 'Tibet': '西藏', 'Hainan': '海南', 'Taiwan': '台湾' + 'SG': '新加坡', 'KR': '韩国', 'DE': '德国', 'FR': '法国', 'GB': '英国' }; const coloNames = { 'NRT': '东京成田', 'HND': '东京羽田', 'KIX': '大阪关西', 'HKG': '香港', - 'SIN': '新加坡', 'ICN': '首尔仁川', 'TPE': '台北桃园', 'PVG': '上海浦东', - 'PEK': '北京首都', 'LAX': '洛杉矶', 'SFO': '旧金山', 'SEA': '西雅图', - 'ORD': '芝加哥', 'DFW': '达拉斯', 'IAD': '华盛顿', 'MIA': '迈阿密', - 'JFK': '纽约', 'LHR': '伦敦', 'CDG': '巴黎', 'FRA': '法兰克福', - 'AMS': '阿姆斯特丹', 'SYD': '悉尼', 'MEL': '墨尔本', 'YYZ': '多伦多', - 'BOM': '孟买', 'DXB': '迪拜', 'BKK': '曼谷', 'KUL': '吉隆坡' + 'SIN': '新加坡', 'ICN': '首尔仁川', 'TPE': '台北桃园', 'LAX': '洛杉矶', + 'SFO': '旧金山', 'SEA': '西雅图', 'ORD': '芝加哥', 'LHR': '伦敦' }; + const continentNames = { 'AS': '亚洲', 'EU': '欧洲', 'NA': '北美', 'SA': '南美', 'AF': '非洲', 'OC': '大洋洲' }; + const i18n = { zh: { subtitle: 'Cloudflare 代理网络', - nodeLocation: '节点代码', - country: '国家', - city: '城市', - status: '状态', - online: '在线', - poolTitle: '代理 IP 池', - ipAddress: 'IP 地址', - latency: '延迟', - regionLabel: '地区', - type: '类型', - apiTitle: 'API 接口', - apiDesc1: '代理请求并返回内容', - apiDesc2: '代理请求并返回 JSON', - apiDesc3: '获取代理 IP 池状态', - apiDesc4: 'Python 客户端代理请求', - apiDesc5: '获取服务状态', - apiDesc6: '获取调试信息', - codeTitle: 'Python 使用示例', - loading: '加载中...', - error: '加载数据失败', - langSwitch: 'EN', - footer: '由 Cloudflare Workers 驱动', - nodeInfoTitle: '节点详情', - visitorInfoTitle: '访问者信息', - visitorIP: '访问者 IP', - regionDetail: '地区', - timezone: '时区', - asn: 'ASN', - coordinates: '坐标', - continent: '大洲', - featuresTitle: '功能特性', - feature1: '全球 300+ 边缘节点', - feature2: 'TLS 指纹模拟', - feature3: '隐身模式', - feature4: 'HTTP/2 支持', - feature5: '浏览器自动化', - feature6: '会话一致性', - installTitle: '快速安装', - version: '版本', - uptime: '运行时间', - responseNote: '响应头包含 X-CF-Colo 和 X-Response-Time' + nodeLocation: '节点代码', country: '国家', city: '城市', status: '状态', online: '在线', + poolTitle: '代理 IP 池', ipAddress: 'IP 地址', latency: '延迟', regionLabel: '地区', type: '类型', + apiTitle: 'API 接口', codeTitle: 'Python 使用示例', loading: '加载中...', + langSwitch: 'EN', footer: '由 Cloudflare Workers 驱动', + nodeInfoTitle: '节点详情', visitorInfoTitle: '访问者信息', visitorIP: '访问者 IP', + timezone: '时区', asn: 'ASN', coordinates: '坐标', continent: '大洲', + feature1: '全球 300+ 边缘节点', feature2: 'TLS 指纹模拟', feature3: '隐身模式', + feature4: 'HTTP/2 支持', feature5: '浏览器自动化', feature6: 'VLESS 协议', + vlessTitle: 'VLESS 代理配置', vlessUUID: 'UUID', vlessHost: '服务器', vlessPort: '端口', + vlessLink: 'VLESS 链接', vlessCopy: '点击复制', version: '版本', + defaultUuidWarning: '⚠️ 安全警告:当前使用默认 UUID,强烈建议通过 Cloudflare Dashboard 设置 UUID 环境变量以提高安全性!', + newIp: '自动新 IP' }, en: { subtitle: 'Cloudflare Proxy Network', - nodeLocation: 'Node Code', - country: 'Country', - city: 'City', - status: 'Status', - online: 'ONLINE', - poolTitle: 'PROXY IP POOL', - ipAddress: 'IP ADDRESS', - latency: 'LATENCY', - regionLabel: 'REGION', - type: 'TYPE', - apiTitle: 'API ENDPOINTS', - apiDesc1: 'Proxy request and return content', - apiDesc2: 'Proxy request and return JSON', - apiDesc3: 'Get proxy IP pool status', - apiDesc4: 'Python client proxy request', - apiDesc5: 'Get service status', - apiDesc6: 'Get debug information', - codeTitle: 'Python Example', - loading: 'LOADING...', - error: 'ERROR LOADING DATA', - langSwitch: '中文', - footer: 'Powered by Cloudflare Workers', - nodeInfoTitle: 'Node Details', - visitorInfoTitle: 'Visitor Information', - visitorIP: 'Visitor IP', - regionDetail: 'Region', - timezone: 'Timezone', - asn: 'ASN', - coordinates: 'Coordinates', - continent: 'Continent', - featuresTitle: 'Features', - feature1: '300+ Global Edge Nodes', - feature2: 'TLS Fingerprint Impersonate', - feature3: 'Stealth Mode', - feature4: 'HTTP/2 Support', - feature5: 'Browser Automation', - feature6: 'Session Consistency', - installTitle: 'Quick Install', - version: 'Version', - uptime: 'Uptime', - responseNote: 'Response headers include X-CF-Colo and X-Response-Time' + nodeLocation: 'Node Code', country: 'Country', city: 'City', status: 'Status', online: 'ONLINE', + poolTitle: 'PROXY IP POOL', ipAddress: 'IP ADDRESS', latency: 'LATENCY', regionLabel: 'REGION', type: 'TYPE', + apiTitle: 'API ENDPOINTS', codeTitle: 'Python Example', loading: 'LOADING...', + langSwitch: '中文', footer: 'Powered by Cloudflare Workers', + nodeInfoTitle: 'Node Details', visitorInfoTitle: 'Visitor Information', visitorIP: 'Visitor IP', + timezone: 'Timezone', asn: 'ASN', coordinates: 'Coordinates', continent: 'Continent', + feature1: '300+ Global Edge Nodes', feature2: 'TLS Fingerprint', feature3: 'Stealth Mode', + feature4: 'HTTP/2 Support', feature5: 'Browser Automation', feature6: 'VLESS Protocol', + vlessTitle: 'VLESS Proxy Config', vlessUUID: 'UUID', vlessHost: 'Server', vlessPort: 'Port', + vlessLink: 'VLESS Link', vlessCopy: 'Click to Copy', version: 'Version', + defaultUuidWarning: '⚠️ SECURITY WARNING: Using default UUID. Strongly recommend setting UUID environment variable via Cloudflare Dashboard for better security!', + newIp: 'Auto New IP' } }; const t = i18n[lang] || i18n.zh; - const switchLang = lang === 'zh' ? 'en' : 'zh'; - const continentNames = { - 'AF': lang === 'zh' ? '非洲' : 'Africa', - 'AN': lang === 'zh' ? '南极洲' : 'Antarctica', - 'AS': lang === 'zh' ? '亚洲' : 'Asia', - 'EU': lang === 'zh' ? '欧洲' : 'Europe', - 'NA': lang === 'zh' ? '北美洲' : 'North America', - 'OC': lang === 'zh' ? '大洋洲' : 'Oceania', - 'SA': lang === 'zh' ? '南美洲' : 'South America' - }; - - const displayColo = lang === 'zh' ? (coloNames[colo] || colo) : colo; - const displayCountry = lang === 'zh' ? (countryNames[country] || country) : country; - const displayCity = lang === 'zh' ? (cityNames[city] || city) : city; - const displayRegion = lang === 'zh' ? (cityNames[region] || region) : region; + const countryName = countryNames[country] || country; + const coloName = coloNames[colo] || colo; + const continentName = continentNames[continent] || continent; + const cityName = city; return ` - - CFSPIDER // PROXY NETWORK -
- ${t.langSwitch} -
- + ${t.langSwitch}
-
-

CFSPIDERv${VERSION}

+
+

CFSPIDERv${VERSION}

${t.subtitle}

-
-
-
${t.nodeLocation}
-
${displayColo}
-
-
-
${t.country}
-
${displayCountry}
-
-
-
${t.city}
-
${displayCity}
-
-
-
${t.continent}
-
${continentNames[continent] || continent}
-
-
-
${t.status}
-
${t.online}
-
-
-
${t.version}
-
${VERSION}
-
+
+
${t.nodeLocation}
${coloName}
+
${t.country}
${countryName}
+
${t.city}
${cityName}
+
${t.continent}
${continentName}
+
${t.status}
${t.online}
+
${t.version}
${VERSION}
// ${t.nodeInfoTitle}
-
- ${t.nodeLocation} - ${displayColo} +
${t.nodeLocation}${coloName}
+
${t.country}${countryName}
+
${t.city}${cityName}
+
${t.asn}AS${asn}
+
${t.timezone}${timezone}
+
${t.coordinates}${latitude}, ${longitude}
-
- ${t.country} - ${displayCountry} -
-
- ${t.city} - ${displayCity} -
-
- ${t.regionDetail} - ${displayRegion} -
-
- ${t.asn} - AS${asn} -
-
- ${t.timezone} - ${timezone} -
-
- ${t.coordinates} - ${latitude}, ${longitude} -
-
-
// ${t.visitorInfoTitle}
-
- ${t.visitorIP} - ${visitorIP} -
-
- ${t.country} - ${displayCountry} -
-
- ${t.asn} - AS${asn} -
-
- ${t.status} - ${t.online} -
+
${t.visitorIP}${visitorIP}
+
${t.country}${countryName}
+
${t.asn}AS${asn}
+
${t.status}${t.online}
-
-
${t.feature1}
+
${t.feature1}
+
${t.feature2}
+
${t.feature3}
+
${t.feature4}
+
${t.feature5}
+
${t.feature6}
-
-
${t.feature2}
+ + ${userID ? ` +
+
// ${t.vlessTitle}
+ ${isDefaultUUID ? ` +
+ ⚠️ +
+
SECURITY WARNING / 安全警告
+
${t.defaultUuidWarning}
+
+ Cloudflare Dashboard → Workers → Settings → Variables → Add UUID
-
-
${t.feature3}
-
-
${t.feature4}
-
-
${t.feature5}
+ ` : ''} +
+
+
// VLESS CONFIG
+
${t.vlessUUID}${userID}${isDefaultUUID ? ' (默认/Default)' : ''}
+
${t.vlessHost}${vlessHost}
+
${t.vlessPort}${vlessPort}
+
TLSenabled
+
TransportWebSocket
+
${t.newIp}ON (Auto)
-
-
${t.feature6}
+
+
// ${t.vlessLink}
+
+
+ ` : ''}
- - - - - - - - - - - - - +

${t.poolTitle}

+
${t.ipAddress}${t.type}${t.status}${t.latency}${t.regionLabel}
${t.loading}
+ +
${t.ipAddress}${t.type}${t.status}${t.latency}${t.regionLabel}
${t.loading}

// ${t.apiTitle}

-
-
- GET - /api/fetch?url=https://example.com -
${t.apiDesc1}
-
-
- GET - /api/json?url=https://httpbin.org/ip -
${t.apiDesc2}
-
-
- GET - /api/pool -
${t.apiDesc3}
-
-
- POST - /proxy?url=...&method=GET -
${t.apiDesc4}
-
-
- GET - /api/status -
${t.apiDesc5}
-
-
- GET - /debug -
${t.apiDesc6}
-
-
-
${t.responseNote}
-
- -
-

// ${t.installTitle}

-
pip install cfspider
+
GET/api/fetch?url=https://example.com
+
GET/api/json?url=https://httpbin.org/ip
+
GET/api/pool
+
POST/proxy?url=...&method=GET
+
GET/api/status
-
# pip install cfspider[extract]
+            
# pip install cfspider
 import cfspider
 
-cf_proxies = "https://your-workers.dev"
-
-# GET request with CF proxy
+# ${isDefaultUUID ? '使用默认 UUID 时,只需填写 Workers 地址' : '使用自定义 UUID 时,需要填写 uuid 参数'}
+for i in range(5):
 response = cfspider.get(
     "https://httpbin.org/ip",
-    cf_proxies=cf_proxies
-)
-print(response.cf_colo)  # Cloudflare node code
-
-# TLS fingerprint + stealth mode
-response = cfspider.get(
-    "https://example.com",
-    impersonate="chrome131",
-    stealth=True
-)
-
-# Data extraction (CSS/XPath/JSONPath)
-title = response.find("h1")
-links = response.find_all("a", attr="href")
-data = response.pick(title="h1", links=("a", "href"))
-data.save("output.csv")
-
-# Batch requests with progress
-urls = ["https://example.com/1", "https://example.com/2"]
-results = cfspider.batch(urls, concurrency=10)
-results.save("results.json")
-
-# Async request (httpx)
-response = await cfspider.aget(
-    "https://httpbin.org/ip",
-    cf_proxies=cf_proxies
-)
-
-# Browser automation (Playwright)
-browser = cfspider.Browser()
-html = browser.html("https://example.com")
-browser.close()
+ cf_proxies="https://${url.hostname}"${isDefaultUUID ? '' : `, + uuid="${userID}"`} + ) + print(response.json()) # 每次都是不同的 IP
-
+

CFSPIDER v${VERSION}

-

+

GITHUB PYPI - DOCS

-

${t.footer}

+

${t.footer}

`; } - -function validateToken(request, env) { - // 如果没有配置 TOKEN 环境变量,跳过验证 - if (!env.TOKEN) { - return { valid: true }; - } - - // 从环境变量读取 token 列表(逗号分隔) - const allowedTokens = env.TOKEN.split(',').map(t => t.trim()).filter(t => t); - - // 如果没有配置任何 token,跳过验证 - if (allowedTokens.length === 0) { - return { valid: true }; - } - - // 从查询参数获取 token - const url = new URL(request.url); - const queryToken = url.searchParams.get('token'); - - // 从 Header 获取 token (Authorization: Bearer xxx) - const authHeader = request.headers.get('Authorization'); - let headerToken = null; - if (authHeader && authHeader.startsWith('Bearer ')) { - headerToken = authHeader.substring(7).trim(); - } - - // 优先使用查询参数,其次使用 Header - const providedToken = queryToken || headerToken; - - // 如果没有提供 token,验证失败 - if (!providedToken) { - return { valid: false }; - } - - // 验证 token 是否在允许列表中 - const isValid = allowedTokens.includes(providedToken); - - return { valid: isValid }; -}