├── .gitignore ├── img └── img_contrast.jpg ├── index.js ├── src ├── proxy-factory.js ├── config.js ├── proxies │ ├── third-party.js │ ├── cloud-provider.js │ ├── fetch.js │ ├── dot.js │ ├── doh.js │ ├── socket.js │ ├── socks5.js │ └── base.js └── main.js ├── ChangeLogs.md ├── proxy.php ├── README.md ├── AIGatewayWithProxyIP半成品.js ├── AIGatewayWithSocks.js └── single.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore test folder 2 | test/ 3 | 4 | # Ignore all .txt files 5 | *.txt -------------------------------------------------------------------------------- /img/img_contrast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XyzenSun/SpectreProxy/HEAD/img/img_contrast.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { ShadowProxy } from './src/main.js'; 2 | 3 | /** 4 | * ShadowProxy启动器 5 | * 为Cloudflare Workers导出fetch事件处理程序 6 | */ 7 | export default { 8 | async fetch(request, env, ctx) { 9 | return await ShadowProxy.handleRequest(request, env, ctx); 10 | } 11 | }; -------------------------------------------------------------------------------- /src/proxy-factory.js: -------------------------------------------------------------------------------- 1 | import { SocketProxy } from './proxies/socket.js'; 2 | import { FetchProxy } from './proxies/fetch.js'; 3 | import { Socks5Proxy } from './proxies/socks5.js'; 4 | import { ThirdPartyProxy } from './proxies/third-party.js'; 5 | import { CloudProviderProxy } from './proxies/cloud-provider.js'; 6 | import { DoHProxy } from './proxies/doh.js'; 7 | import { DoTProxy } from './proxies/dot.js'; 8 | 9 | /** 10 | * 代理工厂类 11 | * 根据配置创建相应的代理实例 12 | */ 13 | export class ProxyFactory { 14 | /** 15 | * 创建代理实例 16 | * @param {object} config - 配置对象 17 | * @returns {BaseProxy} 代理实例 18 | */ 19 | static createProxy(config) { 20 | const strategy = config.PROXY_STRATEGY || 'socket'; 21 | 22 | switch (strategy.toLowerCase()) { 23 | case 'socket': 24 | return new SocketProxy(config); 25 | case 'fetch': 26 | return new FetchProxy(config); 27 | case 'socks5': 28 | return new Socks5Proxy(config); 29 | case 'thirdparty': 30 | return new ThirdPartyProxy(config); 31 | case 'cloudprovider': 32 | return new CloudProviderProxy(config); 33 | case 'doh': 34 | return new DoHProxy(config); 35 | case 'dot': 36 | return new DoTProxy(config); 37 | default: 38 | // 默认Socket代理 39 | return new SocketProxy(config); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /ChangeLogs.md: -------------------------------------------------------------------------------- 1 | # 项目变更记录 2 | 3 | ## 2025-08-19 4 | 5 | ### ✨ 新增功能 (feat) 6 | 7 | 1. **实现 AI API 专用代理 (`AIGateway`)** 8 | * **核心能力**: 实现了专为 AI API 设计的高级代理,完美支持流式传输。 9 | * **智能路由**: 可根据目标域名(如 `api.openai.com`)自动选择 `SOCKS5` 代理或直连,提升性能和成功率。 10 | * **故障回退与重试**: 11 | * 支持 SOCKS5 作为直连失败后的备用方案 (`Fallback`)。 12 | * 对幂等请求(如 GET)在遇到 5xx 错误时自动进行指数退避重试。 13 | * **灵活配置**: 14 | * 支持从多个来源动态获取 SOCKS5 代理并随机选用。 15 | * 支持 URL 预设 (`/openai`, `/gemini`),简化请求路径。 16 | * **请求伪装**: 17 | * 支持随机化 `User-Agent` 和 `Accept-Language` 请求头。 18 | * 能够根据原始请求的 UA 类型(移动/桌面)智能匹配兼容的 UA。 19 | 20 | 2. **探索 SNI 代理模式 (部分实现)** 21 | * 在 `AIGatewayWithProxyIP半成品.js` 文件中,尝试了通过反代 Cloudflare IP 实现 SNI 代理的方案。 22 | * **开发思路**: 23 | * 通过内置的 DoH 解析获取目标域名的真实 IP。 24 | * 维护一个指向可用反代 IP 的域名。(设置较短的TTL,实现简陋的负载均衡) 25 | * 借鉴 `edgetunnel` 的思路,在请求失败时克隆 `socket`,将其转发至反代 IP 并保持原始 `Host` 头。 26 | * **当前障碍**: 在 TLS 握手后出现 `Stream was cancelled` 错误,且暂无稳定获取可用反代 IP 的自动化方案。 27 | 28 | 3. **明确 `single.js` 的未来开发方向** 29 | * `single.js` 将主要面向注重隐私保护的普通用户。 30 | * 将优先考虑支持更多协议,而非极致的性能优化。 31 | * 在对隐私要求不高的场景,将优先使用原生 `fetch` API,以降低 `socket` 编程带来的复杂性。 32 | 33 | ### 🐛 问题修复 (bugfix) 34 | 35 | 1. 修复了在特定场景下流式传输可能中断或不完整的问题。 36 | 37 | ### 🗑️ 废弃与移除 (drop) 38 | 39 | 1. `single.js`**废弃 `proxyip` (SNI 代理) 模式** 40 | * **原因**: 维护一个稳定、可用的反代 IP 池非常困难且耗时。此模式已从 `single.js` 中移除。 41 | * **参考实现**: 对此技术感兴趣的开发者,可以参考 `AIGatewayWithProxyIP半成品.js` 中的探索性代码。 42 | 43 | 2. **放弃实现连接池** 44 | * **原因**: 遇到了 Cloudflare Workers 的核心安全限制——`"Cannot perform I/O on behalf of a different request"` 错误。 45 | * **技术细节**: 一个请求创建的 `socket` 无法被另一个独立的请求复用。可以通过 `Durable Objects` 等方式绕过,但这会引入新的复杂性,并且泄露请求头等敏感信息,违背了隐私保护的初衷。 46 | 47 | 3. **在 `AIGateway` 中移除 WebSocket 支持** 48 | * **原因**: 49 | * **应用场景窄**: 在主流 AI API (OpenAI, Gemini) 中,WebSocket 主要用于实时语音等少数场景,用户基数较小。 50 | * **要求严苛**: 实时应用对延迟和稳定性要求高。 51 | * **兼容性未知**: 无法保证所用的 SOCKS5 服务端或反代 IP 会稳定支持 WebSocket 协议。 52 | * **替代方案**: 有 WebSocket 代理需求的用户请使用 `single.js`。 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置管理器 3 | * 负责处理环境变量和全局配置 4 | */ 5 | export class ConfigManager { 6 | // 默认配置 7 | static DEFAULT_CONFIG = { 8 | // 认证令牌,务必在此处或填入环境变量来修改 9 | AUTH_TOKEN: "your-auth-token", 10 | // 默认目标URL 11 | DEFAULT_DST_URL: "https://httpbin.org/get", 12 | // 调试模式,默认关闭 13 | DEBUG_MODE: false, 14 | // 主代理策略 15 | PROXY_STRATEGY: "socket", 16 | // 回退策略,当主策略不可用时将请求转发到回退策略 17 | // 可选fetch, socks5, thirdparty, cloudprovider,只对HTTP请求有效 18 | // 对于普通用户,建议使用fetch作为回退策略 19 | // 对于希望保护隐私,但不方便自建socks5或第三方代理的用户,建议使用cloudprovider策略 20 | // 对于需要严格保护隐私的用户且有条件自建socks5或第三方代理的用户,建议使用socks5或thirdparty策略 21 | FALLBACK_PROXY_STRATEGY: "fetch", 22 | 23 | // 代理IP 24 | //PROXY_IP: "", //暂未实现,请勿填写 25 | 26 | // SOCKS5代理地址,格式 "host:port" 27 | SOCKS5_ADDRESS: "", 28 | // thirdparty策略的代理地址 29 | THIRD_PARTY_PROXY_URL: "", 30 | // 其他云服务商函数URL 31 | CLOUD_PROVIDER_URL: "", 32 | // DoH服务器配置,默认使用Google的DoH服务器 33 | DOH_SERVER_HOSTNAME: "dns.google", 34 | DOH_SERVER_PORT: 443, 35 | DOH_SERVER_PATH: "/dns-query", 36 | // DoT服务器配置,默认使用Google的DoT服务器 37 | DOT_SERVER_HOSTNAME: "dns.google", 38 | DOT_SERVER_PORT: 853, 39 | }; 40 | 41 | /** 42 | * 从环境变量更新配置 43 | * @param {object} env - 环境变量对象 44 | * @returns {object} 更新后的配置 45 | */ 46 | static updateConfigFromEnv(env) { 47 | if (!env) return { ...this.DEFAULT_CONFIG }; 48 | 49 | const config = { ...this.DEFAULT_CONFIG }; 50 | 51 | for (const key of Object.keys(config)) { 52 | if (key in env) { 53 | if (typeof config[key] === 'boolean') { 54 | config[key] = env[key] === 'true'; 55 | } else { 56 | config[key] = env[key]; 57 | } 58 | } 59 | } 60 | 61 | return config; 62 | } 63 | 64 | /** 65 | * 获取配置值 66 | * @param {object} config - 配置对象 67 | * @param {string} key - 配置键 68 | * @param {*} defaultValue - 默认值 69 | * @returns {*} 配置值 70 | */ 71 | static getConfigValue(config, key, defaultValue = null) { 72 | return config[key] !== undefined ? config[key] : defaultValue; 73 | } 74 | } -------------------------------------------------------------------------------- /src/proxies/third-party.js: -------------------------------------------------------------------------------- 1 | import { BaseProxy } from './base.js'; 2 | 3 | /** 4 | * 第三方代理类 5 | * 使用第三方代理服务进行连接 6 | */ 7 | export class ThirdPartyProxy extends BaseProxy { 8 | /** 9 | * 构造函数 10 | * @param {object} config - 配置对象 11 | */ 12 | constructor(config) { 13 | super(config); 14 | } 15 | 16 | /** 17 | * 连接目标服务器 18 | * @param {Request} req - 请求对象 19 | * @param {string} dstUrl - 目标URL 20 | * @returns {Promise} 响应对象 21 | */ 22 | async connect(req, dstUrl) { 23 | // 检查请求是否为WebSocket请求 24 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 25 | const isWebSocket = upgradeHeader === "websocket"; 26 | 27 | if (isWebSocket) { 28 | // 第三方代理可能不支持WebSocket,返回错误 29 | return new Response("Third party proxy may not support WebSocket", { status: 400 }); 30 | } else { 31 | return await this.connectHttp(req, dstUrl); 32 | } 33 | } 34 | 35 | /** 36 | * 连接HTTP目标服务器 37 | * @param {Request} req - 请求对象 38 | * @param {string} dstUrl - 目标URL 39 | * @returns {Promise} 响应对象 40 | */ 41 | async connectHttp(req, dstUrl) { 42 | const thirdPartyProxyUrl = this.config.THIRD_PARTY_PROXY_URL; 43 | if (!thirdPartyProxyUrl) { 44 | return this.handleError(new Error("Third party proxy URL is not configured"), "Third party proxy connection", 500); 45 | } 46 | 47 | const proxyUrlObj = new URL(thirdPartyProxyUrl); 48 | proxyUrlObj.searchParams.set('target', dstUrl); 49 | 50 | // 创建一个新的请求,直接使用原始头部,不再过滤 51 | const proxyRequest = new Request(proxyUrlObj.toString(), { 52 | method: req.method, 53 | headers: req.headers, 54 | body: req.body, 55 | redirect: 'manual', // 防止代理本身发生重定向 56 | }); 57 | 58 | try { 59 | this.log(`Using third party proxy via fetch to connect to`, dstUrl); 60 | return await fetch(proxyRequest); 61 | } catch (error) { 62 | return this.handleError(error, "Third party proxy connection"); 63 | } 64 | } 65 | 66 | /** 67 | * 连接WebSocket目标服务器 68 | * @param {Request} req - 请求对象 69 | * @param {string} dstUrl - 目标URL 70 | * @returns {Promise} 响应对象 71 | */ 72 | async connectWebSocket(req, dstUrl) { 73 | // 第三方代理可能不支持WebSocket,返回错误 74 | return new Response("Third party proxy may not support WebSocket", { status: 400 }); 75 | } 76 | } -------------------------------------------------------------------------------- /src/proxies/cloud-provider.js: -------------------------------------------------------------------------------- 1 | import { BaseProxy } from './base.js'; 2 | 3 | /** 4 | * 云服务商代理类 5 | * 使用其他云服务商的Serverless函数作为跳板进行连接,避免泄露真实IP,但将泄露其他云服务商的一些信息 6 | * 例如Vercel将会泄露Vercel的Host等信息 7 | * 注意:此代理可能不支持WebSocket连接,各个平台的规则不同 8 | */ 9 | export class CloudProviderProxy extends BaseProxy { 10 | /** 11 | * 构造函数 12 | * @param {object} config - 配置 13 | */ 14 | constructor(config) { 15 | super(config); 16 | } 17 | 18 | /** 19 | * 连接目标服务器 20 | * @param {Request} req - 请求对象 21 | * @param {string} dstUrl - 目标URL 22 | * @returns {Promise} 响应对象 23 | */ 24 | async connect(req, dstUrl) { 25 | // 检查请求是否为WebSocket请求 26 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 27 | const isWebSocket = upgradeHeader === "websocket"; 28 | 29 | if (isWebSocket) { 30 | // 由于无法确认使用的其他云服务商的WebSocket支持情况,直接返回错误 31 | // 如果确定云服务商支持WebSocket,可以实现相应的连接逻辑 32 | return new Response("Cloud provider proxy may not support WebSocket", { status: 400 }); 33 | } else { 34 | return await this.connectHttp(req, dstUrl); 35 | } 36 | } 37 | 38 | /** 39 | * 连接HTTP目标服务器 40 | * @param {Request} req - 请求对象 41 | * @param {string} dstUrl - 目标URL 42 | * @returns {Promise} 响应对象 43 | */ 44 | async connectHttp(req, dstUrl) { 45 | const cloudProviderUrl = this.config.CLOUD_PROVIDER_URL; 46 | if (!cloudProviderUrl) { 47 | return this.handleError(new Error("Cloud provider URL is not configured"), "Cloud provider proxy connection", 500); 48 | } 49 | 50 | const proxyUrlObj = new URL(cloudProviderUrl); 51 | proxyUrlObj.searchParams.set('target', dstUrl); 52 | 53 | // 创建一个新的请求,直接使用原始头部,不再过滤 54 | // 为避免去除头部发送到云服务商时可能导致的问题,此处不再过滤头部 55 | // 应当在云服务商函数中处理头部,去除掉Cloudflare的头部信息 56 | // 可以参考 base.js中的 filterHeaders 方法 57 | const proxyRequest = new Request(proxyUrlObj.toString(), { 58 | method: req.method, 59 | headers: req.headers, 60 | body: req.body, 61 | redirect: 'manual', // 防止代理本身发生重定向 62 | }); 63 | 64 | try { 65 | this.log(`Using cloud provider proxy via fetch to connect to`, dstUrl); 66 | return await fetch(proxyRequest); 67 | } catch (error) { 68 | return this.handleError(error, "Cloud provider proxy connection"); 69 | } 70 | } 71 | 72 | /** 73 | * 连接WebSocket目标服务器 74 | * @param {Request} req - 请求对象 75 | * @param {string} dstUrl - 目标URL 76 | * @returns {Promise} 响应对象 77 | */ 78 | async connectWebSocket(req, dstUrl) { 79 | // 由于无法确认使用的其他云服务商的WebSocket支持情况,直接返回错误 80 | // 如果确定云服务商支持WebSocket,可以实现相应的连接逻辑 81 | return new Response("Cloud provider proxy may not support WebSocket", { status: 400 }); 82 | } 83 | } -------------------------------------------------------------------------------- /proxy.php: -------------------------------------------------------------------------------- 1 | $value) { 20 | $lowerKey = strtolower($key); 21 | $shouldStrip = false; 22 | 23 | // 过滤掉主机头和内容长度头,cURL会处理 24 | if ($lowerKey === 'host' || $lowerKey === 'content-length') { 25 | continue; 26 | } 27 | 28 | // 过滤掉Cloudflare相关的头 29 | foreach ($stripPrefixes as $prefix) { 30 | if (strpos($lowerKey, $prefix) === 0) { 31 | $shouldStrip = true; 32 | break; 33 | } 34 | } 35 | 36 | if (!$shouldStrip) { 37 | $requestHeaders[] = "$key: $value"; 38 | } 39 | } 40 | 41 | // 4. 获取请求体 (适用于 POST, PUT 等方法) 42 | $requestBody = file_get_contents('php://input'); 43 | 44 | // 5. 设置 cURL 选项 45 | curl_setopt($ch, CURLOPT_URL, $targetUrl); 46 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $_SERVER['REQUEST_METHOD']); // 设置请求方法 47 | curl_setopt($ch, CURLOPT_HTTPHEADER, $requestHeaders); // 设置请求头 48 | curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody); // 设置请求体 49 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 将响应作为字符串返回 50 | curl_setopt($ch, CURLOPT_HEADER, true); // 包含响应头 51 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 不自动处理重定向 52 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 在测试环境中禁用SSL证书验证 53 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 54 | 55 | // 6. 执行 cURL 请求 56 | $response = curl_exec($ch); 57 | $curlError = curl_error($ch); 58 | $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 59 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 60 | 61 | if ($curlError) { 62 | http_response_code(500); 63 | die("cURL Error: " . $curlError); 64 | } 65 | 66 | // 7. 关闭 cURL 67 | curl_close($ch); 68 | 69 | // 8. 分离响应头和响应体 70 | $responseHeaders = substr($response, 0, $headerSize); 71 | $responseBody = substr($response, $headerSize); 72 | 73 | // 9. 发送响应头 74 | // 设置HTTP响应码 75 | http_response_code($httpCode); 76 | 77 | $headers = explode("\r\n", $responseHeaders); 78 | foreach ($headers as $header) { 79 | // 过滤掉空的或无效的头,以及传输编码头(由PHP处理) 80 | if ($header && strpos(strtolower($header), 'transfer-encoding') === false) { 81 | header($header, false); 82 | } 83 | } 84 | 85 | // 10. 发送响应体 86 | echo $responseBody; 87 | 88 | ?> -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { ConfigManager } from './config.js'; 2 | import { ProxyFactory } from './proxy-factory.js'; 3 | 4 | /** 5 | * ShadowProxy Main 6 | * 负责处理请求并根据配置选择合适的代理策略 7 | */ 8 | export class ShadowProxy { 9 | /** 10 | * 处理请求的入口 11 | * @param {Request} req - 请求对象 12 | * @param {object} env - 环境变量 13 | * @param {object} ctx - 上下文对象 14 | * @returns {Promise} 响应对象 15 | */ 16 | static async handleRequest(req, env, ctx) { 17 | try { 18 | // 更新配置 19 | const config = ConfigManager.updateConfigFromEnv(env); 20 | 21 | // 解析请求路径 22 | const url = new URL(req.url); 23 | const parts = url.pathname.split("/").filter(Boolean); 24 | 25 | // 检查是否为DNS查询请求 26 | if (parts.length >= 3 && parts[1] === 'dns') { 27 | const auth = parts[0]; 28 | const dnsType = parts[2]; // DNS类型: DOH/DOT 29 | const server = parts[3]; // 可选服务器地址,否则使用默认DOH/DOT服务器 30 | 31 | // 验证AuthToken 32 | if (auth === config.AUTH_TOKEN) { 33 | // 根据DNS类型选择代理策略 34 | let proxyStrategy = config.PROXY_STRATEGY; 35 | if (dnsType === 'doh') { 36 | proxyStrategy = 'doh'; 37 | } else if (dnsType === 'dot') { 38 | proxyStrategy = 'dot'; 39 | } 40 | 41 | // 更新配置以使用相应的DNS代理策略 42 | const dnsConfig = { ...config, PROXY_STRATEGY: proxyStrategy }; 43 | const proxy = ProxyFactory.createProxy(dnsConfig); 44 | 45 | // 处理DNS查询请求 46 | return await proxy.handleDnsQuery(req); 47 | } 48 | } 49 | 50 | // 创建默认代理实例 51 | const proxy = ProxyFactory.createProxy(config); 52 | 53 | // 解析目标URL 54 | const dstUrl = this.parseDestinationUrl(req, config); 55 | 56 | // 使用代理连接目标服务器 57 | return await proxy.connect(req, dstUrl); 58 | } catch (error) { 59 | console.error("ShadowProxy error:", error); 60 | return new Response(`Error: ${error.message}`, { status: 500 }); 61 | } 62 | } 63 | 64 | /** 65 | * 解析目标URL 66 | * @param {Request} req - 请求对象 67 | * @param {object} config - 配置对象 68 | * @returns {string} 目标URL 69 | */ 70 | static parseDestinationUrl(req, config) { 71 | const url = new URL(req.url); 72 | const parts = url.pathname.split("/").filter(Boolean); 73 | const [auth, protocol, ...path] = parts; 74 | 75 | // 检查authtoken 76 | const isValid = auth === config.AUTH_TOKEN; 77 | 78 | let dstUrl = config.DEFAULT_DST_URL; 79 | 80 | if (isValid && protocol) { 81 | // Handle cases where the protocol from the path might be "https:" or "https" 82 | if (protocol.endsWith(':')) { 83 | dstUrl = `${protocol}//${path.join("/")}${url.search}`; 84 | } else { 85 | dstUrl = `${protocol}://${path.join("/")}${url.search}`; 86 | } 87 | } 88 | 89 | // 如果启用了调试模式,记录目标URL 90 | if (config.DEBUG_MODE) { 91 | console.log("Target URL", dstUrl); 92 | } 93 | 94 | return dstUrl; 95 | } 96 | } -------------------------------------------------------------------------------- /src/proxies/fetch.js: -------------------------------------------------------------------------------- 1 | import { BaseProxy } from './base.js'; 2 | 3 | /** 4 | * Fetch代理类 5 | * 使用Fetch API进行连接 6 | */ 7 | export class FetchProxy extends BaseProxy { 8 | /** 9 | * 构造函数 10 | * @param {object} config - 配置对象 11 | */ 12 | constructor(config) { 13 | super(config); 14 | // 上游DNS服务器配置 15 | this.UPSTREAM_DNS_SERVER = { 16 | hostname: config.DOH_SERVER_HOSTNAME || 'dns.google', 17 | path: config.DOH_SERVER_PATH || '/dns-query', 18 | }; 19 | } 20 | 21 | /** 22 | * 连接目标服务器 23 | * @param {Request} req - 请求对象 24 | * @param {string} dstUrl - 目标URL 25 | * @returns {Promise} 响应对象 26 | */ 27 | async connect(req, dstUrl) { 28 | // 检查请求是否为WebSocket请求 29 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 30 | const isWebSocket = upgradeHeader === "websocket"; 31 | 32 | if (isWebSocket) { 33 | // Fetch不支持WebSocket,返回错误 34 | return new Response("Fetch proxy does not support WebSocket", { status: 400 }); 35 | } else { 36 | return await this.connectHttp(req, dstUrl); 37 | } 38 | } 39 | 40 | /** 41 | * 连接HTTP目标服务器 42 | * @param {Request} req - 请求对象 43 | * @param {string} dstUrl - 目标URL 44 | * @returns {Promise} 响应对象 45 | */ 46 | async connectHttp(req, dstUrl) { 47 | const targetUrl = new URL(dstUrl); 48 | 49 | // 清理头部信息 50 | const cleanedHeaders = this.filterHeaders(req.headers); 51 | 52 | // 设置必需的头部 53 | cleanedHeaders.set("Host", targetUrl.hostname); 54 | 55 | try { 56 | // 使用fetch进行连接 57 | const fetchRequest = new Request(dstUrl, { 58 | method: req.method, 59 | headers: cleanedHeaders, 60 | body: req.body, 61 | }); 62 | 63 | this.log("Using fetch to connect to", dstUrl); 64 | return await fetch(fetchRequest); 65 | } catch (error) { 66 | // 使用统一的错误处理方法 67 | return this.handleError(error, "Fetch connection"); 68 | } 69 | } 70 | 71 | /** 72 | * 连接WebSocket目标服务器 73 | * @param {Request} req - 请求对象 74 | * @param {string} dstUrl - 目标URL 75 | * @returns {Promise} 响应对象 76 | */ 77 | async connectWebSocket(req, dstUrl) { 78 | // Fetch不支持WebSocket,返回错误 79 | return new Response("Fetch proxy does not support WebSocket", { status: 400 }); 80 | } 81 | 82 | /** 83 | * 处理DNS查询请求 84 | * @param {Request} req - 请求对象 85 | * @returns {Promise} 响应对象 86 | */ 87 | async handleDnsQuery(req) { 88 | // Fetch代理可以直接处理DNS查询请求 89 | try { 90 | // 构建上游DNS服务器URL 91 | const upstreamDnsUrl = `https://${this.UPSTREAM_DNS_SERVER.hostname}${this.UPSTREAM_DNS_SERVER.path}`; 92 | 93 | // 清理头部信息 94 | const cleanedHeaders = this.filterHeaders(req.headers); 95 | 96 | // 设置必需的头部 97 | cleanedHeaders.set("Host", this.UPSTREAM_DNS_SERVER.hostname); 98 | cleanedHeaders.set("Content-Type", "application/dns-message"); 99 | cleanedHeaders.set("Accept", "application/dns-message"); 100 | 101 | // 使用fetch转发DNS查询请求 102 | const fetchRequest = new Request(upstreamDnsUrl, { 103 | method: req.method, 104 | headers: cleanedHeaders, 105 | body: req.body, 106 | }); 107 | 108 | this.log("Using fetch to handle DNS query"); 109 | return await fetch(fetchRequest); 110 | } catch (error) { 111 | // 使用统一的错误处理方法 112 | return this.handleError(error, "Fetch DNS query handling", 502); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/proxies/dot.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | import { BaseProxy } from './base.js'; 3 | 4 | /** 5 | * DoT 代理类 6 | * 用于代理DOT查询请求 7 | */ 8 | export class DoTProxy extends BaseProxy { 9 | /** 10 | * 构造函数 11 | * @param {object} config - 配置对象 12 | */ 13 | constructor(config) { 14 | super(config); 15 | // 获取上游DoT服务器信息 16 | this.UPSTREAM_DOT_SERVER = { 17 | hostname: config.DOT_SERVER_HOSTNAME || 'some-niche-dns.com', 18 | port: config.DOT_SERVER_PORT || 853, 19 | }; 20 | } 21 | 22 | /** 23 | * 处理DNS查询请求 24 | * @param {Request} req - 请求对象 25 | * @returns {Promise} 响应对象 26 | */ 27 | async handleDnsQuery(req) { 28 | if (req.method !== 'POST' || req.headers.get('content-type') !== 'application/dns-message') { 29 | return new Response('This is a DNS proxy. Please use a DoT client.', { status: 400 }); 30 | } 31 | 32 | let clientDnsQuery; 33 | try { 34 | clientDnsQuery = await req.arrayBuffer(); 35 | const socket = connect(this.UPSTREAM_DOT_SERVER, { secureTransport: 'on', allowHalfOpen: false }); 36 | const writer = socket.writable.getWriter(); 37 | const queryLength = clientDnsQuery.byteLength; 38 | const lengthBuffer = new Uint8Array(2); 39 | new DataView(lengthBuffer.buffer).setUint16(0, queryLength, false); // Big-endian 40 | const dotRequest = new Uint8Array(2 + queryLength); 41 | dotRequest.set(lengthBuffer, 0); 42 | dotRequest.set(new Uint8Array(clientDnsQuery), 2); 43 | await writer.write(dotRequest); 44 | writer.releaseLock(); 45 | const reader = socket.readable.getReader(); 46 | let responseChunks = []; 47 | let totalLength = 0; 48 | 49 | // DoT 的响应可能分片,需要循环读取 50 | while (true) { 51 | const { done, value } = await reader.read(); 52 | if (done) break; 53 | responseChunks.push(value); 54 | totalLength += value.length; 55 | } 56 | 57 | reader.releaseLock(); 58 | await socket.close(); 59 | 60 | // 合并分片 61 | const fullResponse = new Uint8Array(totalLength); 62 | let offset = 0; 63 | for (const chunk of responseChunks) { 64 | fullResponse.set(chunk, offset); 65 | offset += chunk.length; 66 | } 67 | 68 | // 解析 DoT 响应(去掉2字节长度前缀) 69 | const responseLength = new DataView(fullResponse.buffer).getUint16(0, false); 70 | const dnsResponse = fullResponse.slice(2, 2 + responseLength); 71 | 72 | //返回DNS查询结果 73 | return new Response(dnsResponse, { 74 | headers: { 'content-type': 'application/dns-message' }, 75 | }); 76 | 77 | } catch (socketError) { 78 | this.log('DoT socket connection failed, falling back to DoH via fetch.', socketError); 79 | 80 | // DOT使用socket策略请求失败(一般因目标DOT服务器使用了Cloudflare网络),使用Fetch请求DOH作为回退 81 | // 由于使用Fetch请求Cloudflare网络的DOT服务器频繁出现问题,所以使用Fetch请求DOH作为回退 82 | try { 83 | this.log('Attempting DoH fallback...'); 84 | const upstreamDnsUrl = `https://${this.config.DOH_SERVER_HOSTNAME}${this.config.DOH_SERVER_PATH || '/dns-query'}`; 85 | 86 | const dohHeaders = new Headers(); 87 | dohHeaders.set("Host", this.config.DOH_SERVER_HOSTNAME); 88 | dohHeaders.set("Content-Type", "application/dns-message"); 89 | dohHeaders.set("Accept", "application/dns-message"); 90 | 91 | const fallbackRequest = new Request(upstreamDnsUrl, { 92 | method: 'POST', 93 | headers: dohHeaders, 94 | body: clientDnsQuery, 95 | }); 96 | 97 | return await fetch(fallbackRequest); 98 | 99 | } catch (fallbackError) { 100 | this.log('DoH fallback also failed.', fallbackError); 101 | return this.handleError(fallbackError, 'DoT and subsequent DoH fallback'); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * 连接目标服务器 108 | * @param {Request} req - 请求对象 109 | * @param {string} dstUrl - 目标URL 110 | * @returns {Promise} 响应对象 111 | */ 112 | async connect(req, dstUrl) { 113 | // DoT代理专门处理DNS查询请求 114 | return await this.handleDnsQuery(req); 115 | } 116 | 117 | /** 118 | * 连接HTTP目标服务器 119 | * @param {Request} req - 请求对象 120 | * @param {string} dstUrl - 目标URL 121 | * @returns {Promise} 响应对象 122 | */ 123 | async connectHttp(req, dstUrl) { 124 | // DoT代理专门处理DNS查询请求 125 | return await this.handleDnsQuery(req); 126 | } 127 | 128 | /** 129 | * 连接WebSocket目标服务器 130 | * @param {Request} req - 请求对象 131 | * @param {string} dstUrl - 目标URL 132 | * @returns {Promise} 响应对象 133 | */ 134 | async connectWebSocket(req, dstUrl) { 135 | // DoT代理不支持WebSocket 136 | return new Response("DoT proxy does not support WebSocket", { status: 400 }); 137 | } 138 | } -------------------------------------------------------------------------------- /src/proxies/doh.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | import { BaseProxy } from './base.js'; 3 | import { FetchProxy } from './fetch.js'; 4 | 5 | /** 6 | * DoH (DNS over HTTPS) 代理类 7 | * 用于代理DNS查询请求 8 | */ 9 | export class DoHProxy extends BaseProxy { 10 | /** 11 | * 构造函数 12 | * @param {object} config - 配置对象 13 | */ 14 | constructor(config) { 15 | super(config); 16 | // 上游DoH服务器信息 17 | this.UPSTREAM_DOH_SERVER = { 18 | hostname: config.DOH_SERVER_HOSTNAME || 'dns.google', 19 | port: config.DOH_SERVER_PORT || 443, 20 | path: config.DOH_SERVER_PATH || '/dns-query', 21 | }; 22 | } 23 | 24 | /** 25 | * 处理DNS查询请求 26 | * @param {Request} req - 请求对象 27 | * @returns {Promise} 响应对象 28 | */ 29 | async handleDnsQuery(req) { 30 | if (req.method !== 'POST' || req.headers.get('content-type') !== 'application/dns-message') { 31 | return new Response('This is a DNS proxy. Please use a DoH client.', { status: 400 }); 32 | } 33 | 34 | let clientDnsQuery; 35 | try { 36 | clientDnsQuery = await req.arrayBuffer(); 37 | 38 | // 过滤请求头,确保不泄露敏感信息 39 | const cleanedHeaders = this.filterHeaders(req.headers); 40 | 41 | // DOH请求头 42 | cleanedHeaders.set('Host', this.UPSTREAM_DOH_SERVER.hostname); 43 | cleanedHeaders.set('Content-Type', 'application/dns-message'); 44 | cleanedHeaders.set('Content-Length', clientDnsQuery.byteLength.toString()); 45 | cleanedHeaders.set('Accept', 'application/dns-message'); 46 | cleanedHeaders.set('Connection', 'close'); // 完成后关闭连接,简化处理 47 | 48 | // 建立TLS连接 49 | const socket = connect(this.UPSTREAM_DOH_SERVER, { secureTransport: 'on', allowHalfOpen: false }); 50 | const writer = socket.writable.getWriter(); 51 | 52 | // 构建HTTP POST请求 53 | const httpHeaders = 54 | `POST ${this.UPSTREAM_DOH_SERVER.path} HTTP/1.1\r\n` + 55 | Array.from(cleanedHeaders.entries()) 56 | .map(([k, v]) => `${k}: ${v}`) 57 | .join('\r\n') + 58 | '\r\n\r\n'; 59 | 60 | const requestHeaderBytes = this.encoder.encode(httpHeaders); 61 | const requestBodyBytes = new Uint8Array(clientDnsQuery); 62 | 63 | // 合并请求头和请求体 64 | const fullRequest = new Uint8Array(requestHeaderBytes.length + requestBodyBytes.length); 65 | fullRequest.set(requestHeaderBytes, 0); 66 | fullRequest.set(requestBodyBytes, requestHeaderBytes.length); 67 | 68 | // 请求 69 | await writer.write(fullRequest); 70 | writer.releaseLock(); 71 | 72 | // 读取并解析响应 73 | const reader = socket.readable.getReader(); 74 | let responseBytes = new Uint8Array(); 75 | 76 | while (true) { 77 | const { done, value } = await reader.read(); 78 | if (done) break; 79 | 80 | // 合并数据块 81 | const newBuffer = new Uint8Array(responseBytes.length + value.length); 82 | newBuffer.set(responseBytes, 0); 83 | newBuffer.set(value, responseBytes.length); 84 | responseBytes = newBuffer; 85 | } 86 | 87 | reader.releaseLock(); 88 | await socket.close(); 89 | 90 | // 5. 剥离HTTP响应头,提取Body,只返回DNS响应结果 91 | const separator = new Uint8Array([13, 10, 13, 10]); 92 | let separatorIndex = -1; 93 | for (let i = 0; i < responseBytes.length - 3; i++) { 94 | if (responseBytes[i] === separator[0] && responseBytes[i + 1] === separator[1] && 95 | responseBytes[i + 2] === separator[2] && responseBytes[i + 3] === separator[3]) { 96 | separatorIndex = i; 97 | break; 98 | } 99 | } 100 | 101 | if (separatorIndex === -1) { 102 | throw new Error("Could not find HTTP header/body separator in response."); 103 | } 104 | 105 | const dnsResponseBody = responseBytes.slice(separatorIndex + 4); 106 | 107 | // 返回DNS响应 108 | return new Response(dnsResponseBody, { 109 | headers: { 'content-type': 'application/dns-message' }, 110 | }); 111 | } catch (error) { 112 | // socket策略失败时,回退到fetch策略 113 | try { 114 | const fallbackProxy = new FetchProxy(this.config); 115 | // 不能重用原始请求,它的主体已被读取。我们使用已有的主体数据创建一个新的请求。 116 | const fallbackRequest = new Request(req.url, { 117 | method: req.method, 118 | headers: req.headers, 119 | body: clientDnsQuery // 之前读取的缓冲区 120 | }); 121 | return await fallbackProxy.handleDnsQuery(fallbackRequest); 122 | } catch (fallbackError) { 123 | return this.handleError(fallbackError, "DoH proxying with connect", 502); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * 连接目标服务器 130 | * @param {Request} req - 请求对象 131 | * @param {string} dstUrl - 目标URL 132 | * @returns {Promise} 响应对象 133 | */ 134 | async connect(req, dstUrl) { 135 | // DoH代理专门处理DNS查询请求 136 | return await this.handleDnsQuery(req); 137 | } 138 | 139 | /** 140 | * 连接HTTP目标服务器 141 | * @param {Request} req - 请求对象 142 | * @param {string} dstUrl - 目标URL 143 | * @returns {Promise} 响应对象 144 | */ 145 | async connectHttp(req, dstUrl) { 146 | // DoH代理专门处理DNS查询请求 147 | return await this.handleDnsQuery(req); 148 | } 149 | 150 | /** 151 | * 连接WebSocket目标服务器 152 | * @param {Request} req - 请求对象 153 | * @param {string} dstUrl - 目标URL 154 | * @returns {Promise} 响应对象 155 | */ 156 | async connectWebSocket(req, dstUrl) { 157 | // DoH代理不支持WebSocket 158 | return new Response("DoH proxy does not support WebSocket", { status: 400 }); 159 | } 160 | } -------------------------------------------------------------------------------- /src/proxies/socket.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | import { BaseProxy } from './base.js'; 3 | import { FetchProxy } from './fetch.js'; 4 | import { Socks5Proxy } from './socks5.js'; 5 | import { ThirdPartyProxy } from './third-party.js'; 6 | import { CloudProviderProxy } from './cloud-provider.js'; 7 | 8 | /** 9 | * Socket代理类 10 | * 使用Cloudflare Socket API进行连接 11 | */ 12 | export class SocketProxy extends BaseProxy { 13 | /** 14 | * 构造函数 15 | * @param {object} config - 配置对象 16 | */ 17 | constructor(config) { 18 | super(config); 19 | } 20 | 21 | /** 22 | * 连接目标服务器 23 | * @param {Request} req - 请求对象 24 | * @param {string} dstUrl - 目标URL 25 | * @returns {Promise} 响应对象 26 | */ 27 | async connect(req, dstUrl) { 28 | // 检查请求是否为WebSocket请求 29 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 30 | const isWebSocket = upgradeHeader === "websocket"; 31 | 32 | if (isWebSocket) { 33 | return await this.connectWebSocket(req, dstUrl); 34 | } else { 35 | return await this.connectHttp(req, dstUrl); 36 | } 37 | } 38 | 39 | /** 40 | * 连接WebSocket目标服务器 41 | * @param {Request} req - 请求对象 42 | * @param {string} dstUrl - 目标URL 43 | * @returns {Promise} 响应对象 44 | */ 45 | async connectWebSocket(req, dstUrl) { 46 | const targetUrl = new URL(dstUrl); 47 | 48 | // 如果目标URL不支持WebSocket协议,返回错误响应 49 | if (!/^wss?:\/\//i.test(dstUrl)) { 50 | return new Response("Target does not support WebSocket", { status: 400 }); 51 | } 52 | 53 | const isSecure = targetUrl.protocol === "wss:"; 54 | const port = targetUrl.port || (isSecure ? 443 : 80); 55 | 56 | // 建立到目标服务器的原始套接字连接 57 | const socket = await connect( 58 | { hostname: targetUrl.hostname, port: Number(port) }, 59 | { secureTransport: isSecure ? "on" : "off", allowHalfOpen: false } 60 | ); 61 | 62 | // 生成WebSocket握手所需的密钥 63 | const key = this.generateWebSocketKey(); 64 | 65 | // 清理头部信息 66 | const cleanedHeaders = this.filterHeaders(req.headers); 67 | 68 | // 构建握手所需的HTTP头部 69 | cleanedHeaders.set('Host', targetUrl.hostname); 70 | cleanedHeaders.set('Connection', 'Upgrade'); 71 | cleanedHeaders.set('Upgrade', 'websocket'); 72 | cleanedHeaders.set('Sec-WebSocket-Version', '13'); 73 | cleanedHeaders.set('Sec-WebSocket-Key', key); 74 | 75 | // 组装WebSocket握手的HTTP请求数据 76 | const handshakeReq = 77 | `GET ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 78 | Array.from(cleanedHeaders.entries()) 79 | .map(([k, v]) => `${k}: ${v}`) 80 | .join('\r\n') + 81 | '\r\n\r\n'; 82 | 83 | this.log("Sending WebSocket handshake request", handshakeReq); 84 | const writer = socket.writable.getWriter(); 85 | await writer.write(this.encoder.encode(handshakeReq)); 86 | 87 | const reader = socket.readable.getReader(); 88 | const handshakeResp = await this.readUntilDoubleCRLF(reader); 89 | this.log("Received handshake response", handshakeResp); 90 | 91 | // 验证握手响应是否表明101 Switching Protocols状态 92 | if ( 93 | !handshakeResp.includes("101") || 94 | !handshakeResp.includes("Switching Protocols") 95 | ) { 96 | throw new Error("WebSocket handshake failed: " + handshakeResp); 97 | } 98 | 99 | // 创建内部WebSocketPair 100 | const webSocketPair = new WebSocketPair(); 101 | const client = webSocketPair[0]; 102 | const server = webSocketPair[1]; 103 | client.accept(); 104 | 105 | // 在客户端和远程套接字之间建立双向帧中继 106 | this.relayWebSocketFrames(client, socket, writer, reader); 107 | return new Response(null, { status: 101, webSocket: server }); 108 | } 109 | 110 | /** 111 | * 连接HTTP目标服务器 112 | * @param {Request} req - 请求对象 113 | * @param {string} dstUrl - 目标URL 114 | * @returns {Promise} 响应对象 115 | */ 116 | async connectHttp(req, dstUrl) { 117 | // 为可能的回退操作,立即克隆请求以保留其body流 118 | const reqForFallback = req.clone(); 119 | const targetUrl = new URL(dstUrl); 120 | 121 | // 清理头部信息 122 | const cleanedHeaders = this.filterHeaders(req.headers); 123 | 124 | // 对于标准HTTP请求:设置必需的头部(如Host并禁用压缩) 125 | cleanedHeaders.set("Host", targetUrl.hostname); 126 | cleanedHeaders.set("accept-encoding", "identity"); 127 | 128 | try { 129 | const port = targetUrl.protocol === "https:" ? 443 : 80; 130 | const socket = await connect( 131 | { hostname: targetUrl.hostname, port: Number(port) }, 132 | { secureTransport: targetUrl.protocol === "https:" ? "on" : "off", allowHalfOpen: false } 133 | ); 134 | const writer = socket.writable.getWriter(); 135 | 136 | // 构建请求行和头部 137 | const requestLine = 138 | `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 139 | Array.from(cleanedHeaders.entries()) 140 | .map(([k, v]) => `${k}: ${v}`) 141 | .join("\r\n") + 142 | "\r\n\r\n"; 143 | 144 | this.log("Sending request", requestLine); 145 | await writer.write(this.encoder.encode(requestLine)); 146 | 147 | // 如果有请求体,将其转发到目标服务器 148 | if (req.body) { 149 | this.log("Forwarding request body"); 150 | const reader = req.body.getReader(); 151 | while (true) { 152 | const { done, value } = await reader.read(); 153 | if (done) break; 154 | await writer.write(value); 155 | } 156 | } 157 | 158 | // 解析并返回目标服务器的响应 159 | return await this.parseResponse(socket.readable.getReader()); 160 | } catch (error) { 161 | // 检查是否是Cloudflare网络限制错误 162 | if (this.isCloudflareNetworkError(error)) { 163 | this.log("Cloudflare network restriction detected, switching to fallback proxy"); 164 | this.log("Original error:", error.message); 165 | 166 | // 根据环境变量配置的备用策略选择合适的备用方案 167 | const fallbackStrategy = this.config.FALLBACK_PROXY_STRATEGY || "fetch"; 168 | this.log("Using fallback strategy:", fallbackStrategy); 169 | 170 | // 创建备用代理实例 171 | const fallbackConfig = { ...this.config, PROXY_STRATEGY: fallbackStrategy }; 172 | let fallbackProxy; 173 | 174 | // 根据备用策略创建相应的代理实例 175 | switch (fallbackStrategy.toLowerCase()) { 176 | case 'fetch': 177 | fallbackProxy = new FetchProxy(fallbackConfig); 178 | break; 179 | case 'socks5': 180 | fallbackProxy = new Socks5Proxy(fallbackConfig); 181 | break; 182 | case 'thirdparty': 183 | fallbackProxy = new ThirdPartyProxy(fallbackConfig); 184 | break; 185 | case 'cloudprovider': 186 | fallbackProxy = new CloudProviderProxy(fallbackConfig); 187 | break; 188 | default: 189 | fallbackProxy = new FetchProxy(fallbackConfig); 190 | } 191 | 192 | this.log("Attempting fallback connection with", fallbackStrategy); 193 | 194 | // 使用备用代理 195 | // 使用克隆出来的、body未被动过的请求来进行回退 196 | return await fallbackProxy.connectHttp(reqForFallback, dstUrl); 197 | } 198 | 199 | // 使用统一的错误处理方法 200 | return this.handleError(error, "Socket connection"); 201 | } 202 | } 203 | 204 | /** 205 | * 检查是否为Cloudflare网络限制错误 206 | * @param {Error} error - 错误对象 207 | * @returns {boolean} 是否为Cloudflare网络限制错误 208 | */ 209 | isCloudflareNetworkError(error) { 210 | // Cloudflare网络限制错误通常包含特定的错误消息 211 | return error.message && ( 212 | error.message.includes("A network issue was detected") || 213 | error.message.includes("Network connection failure") || 214 | error.message.includes("connection failed") || 215 | error.message.includes("timed out") || 216 | error.message.includes("Stream was cancelled") || 217 | error.message.includes("proxy request failed") || 218 | error.message.includes("cannot connect to the specified address") || 219 | error.message.includes("TCP Loop detected") || 220 | error.message.includes("Connections to port 25 are prohibited") 221 | ); 222 | } 223 | 224 | async handleDnsQuery(req) { 225 | // Socket代理不直接处理DNS查询请求,需要使用DoH或DoT代理 226 | return new Response("Socket proxy does not support DNS query handling. Please use DoH or DoT proxy.", { status: 400 }); 227 | } 228 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpectreProxy 2 | 3 | ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)![Language](https://img.shields.io/badge/language-JavaScript-orange.svg)![Platform](https://img.shields.io/badge/platform-Cloudflare%20Workers-red) 4 | 5 | 一个基于 Cloudflare Workers 和原生 TCP Socket 的高级智能代理网关。它通过完全控制请求流,解决了原生 `fetch` API 存在的隐私泄露问题,并提供了灵活的回退策略、智能路由和多协议支持,专为需要高稳定性、隐私保护和复杂网络访问的场景设计。 6 | 7 | ## 目录 8 | - [免责声明](#免责声明) 9 | - [项目背景](#项目背景) 10 | - [核心原理与特性](#核心原理与特性) 11 | - [核心原理](#核心原理) 12 | - [主要特性](#主要特性-1) 13 | - [部署指南](#部署指南) 14 | - [版本选择](#版本选择) 15 | - [部署步骤](#部署步骤) 16 | - [配置环境变量](#配置环境变量) 17 | - [环境变量配置](#环境变量配置-1) 18 | - [通用配置 (single.js & AIGateway)](#通用配置-singlejs--aigateway) 19 | - [AIGateway 专属配置](#aigateway-专属配置) 20 | - [single.js 专属配置](#singlejs-专属配置) 21 | - [使用方法](#使用方法) 22 | - [AIGateway (AI 代理优化版)](#aigateway-ai-代理优化版) 23 | - [single.js (通用版)](#singlejs-通用版) 24 | - [兼容性测试](#兼容性测试) 25 | - [开发指南](#开发指南) 26 | - [项目结构](#项目结构) 27 | - [第三方HTTP代理](#第三方http代理) 28 | - [其他云平台兼容代理](#其他云平台兼容代理) 29 | - [变更记录 (Changelog)](#变更记录-changelog) 30 | - [License](#license) 31 | 32 | ## 免责声明 33 | 34 | 本项目是一个用于学习和理解 Cloudflare Worker 机制的示例,仅供个人学习和研究网络技术之用,**严禁用于任何非法目的**。 35 | 36 | 任何使用者在使用本项目时,均应遵守其所在国家或地区的法律法规。对于任何因使用或滥用本项目而导致的任何直接或间接的法律责任和风险,**均由使用者本人承担**,作者及贡献者不对任何第三方使用本项目进行的任何非法活动及其造成的任何损害承担责任。 37 | 38 | 如果您开始使用本项目,即表示您已充分理解并同意上述条款。 39 | 40 | ## 项目背景 41 | 42 | Cloudflare Workers 的 `fetch` API 是一个功能强大的工具,可以轻松实现反向代理。然而,它会自动在请求中添加特定的 `CF-*` 请求头,例如: 43 | 44 | - `cf-connecting-ip`:暴露发起请求用户的真实 IP 地址。 45 | - `cf-ipcountry`:暴露用户所在的国家/地区。 46 | - `cf-worker`:明确标识该请求经过了 Cloudflare Workers。 47 | 48 | 这些请求头无法通过常规方式移除,会导致以下问题: 49 | 50 | 1. **隐私泄露**:用户的真实 IP 被暴露给目标服务器。 51 | 2. **访问限制**:部分对国家/地区有限制的服务(如 OpenAI, Claude)会因 `cf-country` 而拒绝访问。 52 | 3. **代理暴露**:目标网站可以轻易识别出请求来自 Cloudflare Workers,甚至可能因此封禁域名。 53 | 54 | ![与fetch api对比](https://github.com/XyzenSun/SpectreProxy/raw/main/img/img_contrast.jpg) 55 | 56 | ## 核心原理与特性 57 | 58 | 为解决上述问题,SpectreProxy 采用了更底层的解决方案。 59 | 60 | ### 核心原理 61 | 62 | 项目通过 Cloudflare Workers 的 **原生 TCP Socket API** (`connect()`) 直接构建 HTTP/1.1、WebSocket 及 DNS 请求。这种方式可以完全控制请求的每一个字节,从根源上避免了 `fetch` API 自动添加的、可能泄露隐私的请求头。 63 | 64 | ### 主要特性 65 | 66 | - **隐私保护**:通过原生 Socket 隐藏真实 IP 和代理痕迹,保护用户隐私。 67 | - **智能路由与回退**: 68 | - **`AIGateway`**: 可根据目标域名自动选择 **SOCKS5 代理** 或 **直连**,并支持直连失败后自动回退到 SOCKS5。 69 | - **`single.js`**: 支持 `socket`, `fetch`, `socks5` 等多种策略,并可在主策略失败时切换到备用策略。 70 | - **多协议支持 (`single.js`)**:原生支持 HTTP/S、WebSocket (WSS),以及 DNS-over-HTTPS (DoH) / DNS-over-TLS (DoT) 查询。 71 | - **高级请求伪装 (`AIGateway`)**: 支持随机化 `User-Agent` 和 `Accept-Language`,并能根据原始请求的 UA 类型(移动/桌面)智能匹配。 72 | - **高可用性 (`AIGateway`)**: 73 | - 支持从多个 API 地址动态获取 SOCKS5 代理并随机选用。 74 | - 对幂等请求在遇到 `5xx` 错误时自动进行指数退避重试。 75 | - **易于部署**:提供单文件 Worker 脚本,无需复杂的外部依赖。 76 | 77 | ## 部署指南 78 | 79 | ### 版本选择 80 | 81 | 项目提供两个版本,请根据需求选择: 82 | 83 | - **`aigateway.js`**: **推荐用于代理 AI API**。经过深度优化,内置智能路由、SOCKS5 代理、自动重试和头部伪装,性能和稳定性更佳。 84 | - **`single.js`**: **通用版本**。支持 HTTP, WebSocket, DoH/DoT 等多种协议,配置灵活,适合更广泛的代理场景。 85 | 86 | ### 部署步骤 87 | 88 | 1. **准备 Cloudflare 环境**:确保你拥有一个 Cloudflare 账号,并已激活 Workers 服务。 89 | 2. **创建 Worker**:在 Cloudflare 控制面板中,`计算(Workers)`->`Workers 和 Pages` > `创建`->`从 Hello World! 开始`->`部署` -> `继续处理项目` -> `编辑代码`。 90 | 3. **复制代码**:复制所选版本(`aigateway.js` 或 `single.js`)的全部内容,粘贴到 `worker.js` 中,并点击 **部署**。 91 | 4. **配置环境变量**:在 Worker 的 `设置` > `变量` 页面中,根据下文说明添加环境变量以完成配置。 92 | 93 | ## 环境变量配置 94 | 95 | ### 通用配置 (single.js & AIGateway) 96 | 97 | | 环境变量 | 说明 | 98 | | ----------------- | ------------------------------------------------------------ | 99 | | `AUTH_TOKEN` | **访问代理所需的认证密钥,务必修改为你自己的强密码**。默认值为 `"your-auth-token"` 或 `"defaulttoken"`。 | 100 | | `DEFAULT_DST_URL` | 当不指定目标时,默认访问的 URL。 | 101 | | `DEBUG_MODE` | 是否开启调试模式,开启后会在控制台输出详细日志。建议生产环境设为 `false`。 | 102 | 103 | ### AIGateway 专属配置 104 | 105 | | 环境变量 | 默认值 | 说明 | 106 | | -------------------------------------- | ------- | --------------------------------------- | 107 | | `ENABLE_UA_RANDOMIZATION` | `true` | 是否启用随机 User-Agent 功能。 | 108 | | `ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION` | `false` | 是否启用随机 `Accept-Language` 功能。 | 109 | | `ENABLE_SOCKS5_FALLBACK` | `true` | 当直连失败时,是否自动尝试使用 SOCKS5。 | 110 | 111 | **提示**: `AIGateway` 的高级路由和 SOCKS5 源在代码内配置 112 | 113 | `SOCKS5_API_URLS`:表示通过Get请求的SOCKS5API路径,如果需要鉴权,修改`parseSocks5Proxy()`中 114 | 115 | ` const response = await fetch(randomApiUrl, { method: 'GET' });` 116 | 117 | `HOST_REQUEST_CONFIG`:通过HOST进行分流,nativeFetch表示使用socket手动实现的http请求,失败了会自动用socks5回退,socks5表示直接用socks5进行代理请求,适用于使用了cloudflare的网站,避免重试,提升性能。 118 | 119 | `URL_PRESETS`:URL路径映射,例如`"gemini", "https://generativelanguage.googleapis.com"` 相当于把`https://your-worker.workers.dev/your-token/gemini` 映射到 `https://generativelanguage.googleapis.com` ; 请求` https://your-worker.workers.dev/your-token/gemini `相当于请求`https://your-worker.workers.dev/your-token/https://generativelanguage.googleapis.com` 120 | 121 | ### single.js 专属配置 122 | 123 | | 环境变量 | 默认值 | 说明 | 124 | | ------------------------- | -------------- | ------------------------------------------------------------ | 125 | | `PROXY_STRATEGY` | `"socket"` | **主代理策略**。可选值: `socket`, `fetch`, `socks5` 等。 | 126 | | `FALLBACK_PROXY_STRATEGY` | `"fetch"` | **回退代理策略**。当主策略失败时启用。 | 127 | | `SOCKS5_ADDRESS` | `""` | SOCKS5 代理地址,格式为 `host:port`或`user:password@host:port`。 | 128 | | `THIRD_PARTY_PROXY_URL` | `""` | 第三方 HTTP 代理的 URL。 | 129 | | `CLOUD_PROVIDER_URL` | `""` | 部署在其他云服务商的兼容代理 URL。 | 130 | | `DOH_SERVER_HOSTNAME` | `"dns.google"` | DoH 服务器的主机名。 | 131 | | ... | ... | (更多 DNS 相关配置请参考 `single.js` 代码) | 132 | 133 | ## 使用方法 134 | 135 | 136 | 137 | ### AIGateway (AI 代理优化版) 138 | 139 | #### 1. 使用 URL 预设 (推荐) 140 | 代码内置了常用 AI 服务的别名,这是最简单方便的使用方式。 141 | **URL 格式:** `https://<你的Worker地址>/<认证令牌>/<预设别名>/` 142 | 143 | **示例:** 144 | - **OpenAI:** `https://your-worker.workers.dev/your-token/openai` 145 | - **Gemini:** `https://your-worker.workers.dev/your-token/gemini` 146 | 147 | #### 2. 使用完整 URL 148 | **URL 格式:** `https://<你的Worker地址>/<认证令牌>/<完整的目标URL>` 149 | 150 | ### single.js (通用版) 151 | 152 | 通过构造特定的 URL 路径来使用代理功能。 153 | **URL 基本格式:** `https://<你的Worker地址>/<认证令牌>/<完整的目标URL>` 154 | 155 | #### 1. HTTP/HTTPS 代理 156 | **URL 格式:** `https:////被代理的URL(包含https://或http://协议头)` 157 | 158 | #### 2. WebSocket (WSS) 代理 159 | **URL 格式:** `wss:////ws/` 160 | 161 | #### 3. DoH/DOT 162 | **URL 格式:** `https:////dns/doh或dot` (详见代码) 163 | 164 | 165 | ## 兼容性测试 166 | - **Google Gemini**: ✅ 167 | - **OpenAI**: ✅ 168 | - **Anthropic Claude**: ✅ 169 | - **NewAPI / One API 等聚合平台**: ✅ 170 | - **GPT-LOAD**:✅ 171 | - **Gemini-balance:✅** 172 | 173 | ## 开发指南 174 | 175 | ### 项目结构 176 | (此结构为完整开发版,单文件版本已将所有代码合并) 177 | ``` 178 | ├── README.md # 项目说明文档 179 | ├── aigateway%.js # AI代理优化版,单文件部署 180 | ├── single.js # 通用版,单文件部署 181 | └── src/ # 源代码目录 182 | ... 183 | ``` 184 | 185 | ### 第三方HTTP代理 186 | (适用于 `single.js` 的 `thirdparty` 策略) 187 | 188 | 需要在第三方代理中实现:接收来自 Cloudflare Worker 的特殊 HTTP 请求,解析 URL中的 `target` 参数,然后向目标服务器发起新请求,并将响应原封不动地返回。 189 | 190 | **关键点:** 191 | 1. **获取 `target` 参数**: `[您的代理URL]?target=[原始目标URL]` 192 | 2. **移除请求头**: 转发时移除 `Host`, `cf-*`, `x-forwarded-*` 等头。 193 | 3. **返回响应**: 将目标服务器的响应(状态码、头、体)完整传回,移除 `Transfer-Encoding` 头。 194 | 195 | ### 其他云平台兼容代理 196 | (适用于 `single.js` 的 `cloudprovider` 策略) 197 | 198 | **核心工作原理**与第三方HTTP代理一致,但通常部署在 Vercel, Zeabur 等 Serverless 平台。可参考 `single.js` 内的示例代码进行开发。 199 | 200 | 您需要创建一个 Serverless Function,它会暴露一个公开的 URL (例如 `api/proxy.js`)。 201 | 202 | 当 Cloudflare Worker 调用它时,会在 URL 中附加一个关键参数 **`target`**,格式为: `[您的函数URL]?target=[原始目标URL]` 203 | 204 | 您的函数代码需要获取 `target` URL、请求方法、头和体。最核心的部分是使用 `fetch` API 将请求转发到 `target`。**关键在于处理请求头**: 205 | 206 | - **必须移除 `Host` 头**,因为 `fetch` 会自动生成正确的 `Host`。 207 | - 建议移除 `cf-*` 和 `x-forwarded-*` 系列的头,以隐藏代理链。 208 | 209 | 最后,将从目标服务器获取的 `Response` 对象**直接返回**即可,Serverless 平台会自动高效地处理流式响应。 210 | 211 | 示例代码,请勿直接使用,建议混淆: 212 | 213 | ``` 214 | export default async function handler(request) { 215 | // 1. 获取 target URL 216 | const { searchParams } = new URL(request.url); 217 | const targetUrl = searchParams.get('target'); 218 | 219 | if (!targetUrl) { 220 | return new Response('Error: Missing "target" URL parameter.', { status: 400 }); 221 | } 222 | 223 | // 安全检查,确保 target 是有效的 http/https URL 224 | try { 225 | const url = new URL(targetUrl); 226 | if (url.protocol !== 'http:' && url.protocol !== 'https:') { 227 | throw new Error('Invalid protocol.'); 228 | } 229 | } catch (err) { 230 | return new Response('Error: Invalid "target" URL parameter.', { status: 400 }); 231 | } 232 | 233 | // 2. 构造转发请求的头部 234 | const forwardHeaders = new Headers(request.headers); 235 | forwardHeaders.delete('host'); // fetch 会自动设置 236 | 237 | for (const [key, value] of request.headers.entries()) { 238 | if (key.toLowerCase().startsWith('cf-') || key.toLowerCase().startsWith('x-forwarded-')) { 239 | forwardHeaders.delete(key); 240 | } 241 | } 242 | 243 | try { 244 | // 3. 使用 fetch 转发请求 245 | const response = await fetch(targetUrl, { 246 | method: request.method, 247 | headers: forwardHeaders, 248 | body: request.body, 249 | redirect: 'manual', // 必须设置,防止 fetch 自动处理重定向 250 | }); 251 | 252 | // 4. 直接返回从目标服务器获取的响应 253 | return response; 254 | 255 | } catch (error) { 256 | return new Response(`Error connecting to target: ${error.message}`, { status: 502 }); 257 | } 258 | } 259 | 260 | // 对于 Vercel Edge Runtime, 需额外配置 261 | export const config = { 262 | runtime: 'edge', 263 | }; 264 | ``` 265 | 266 | ## 变更记录 (Changelog) 267 | 268 | **请访问** https://github.com/XyzenSun/SpectreProxy/blob/main/ChangeLogs.md 269 | 270 | ## License 271 | 272 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /src/proxies/socks5.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | import { BaseProxy } from './base.js'; 3 | 4 | /** 5 | * SOCKS5代理类 6 | * 使用SOCKS5代理进行连接 7 | */ 8 | export class Socks5Proxy extends BaseProxy { 9 | /** 10 | * 构造函数 11 | * @param {object} config - 配置对象 12 | */ 13 | constructor(config) { 14 | super(config); 15 | this.parsedSocks5Address = this.parseSocks5Address(config.SOCKS5_ADDRESS); 16 | } 17 | 18 | /** 19 | * 连接目标服务器 20 | * @param {Request} req - 请求对象 21 | * @param {string} dstUrl - 目标URL 22 | * @returns {Promise} 响应对象 23 | */ 24 | async connect(req, dstUrl) { 25 | // 检查请求是否为WebSocket请求 26 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 27 | const isWebSocket = upgradeHeader === "websocket"; 28 | 29 | if (isWebSocket) { 30 | return await this.connectWebSocket(req, dstUrl); 31 | } else { 32 | return await this.connectHttp(req, dstUrl); 33 | } 34 | } 35 | 36 | /** 37 | * 连接WebSocket目标服务器 38 | * @param {Request} req - 请求对象 39 | * @param {string} dstUrl - 目标URL 40 | * @returns {Promise} 响应对象 41 | */ 42 | async connectWebSocket(req, dstUrl) { 43 | const targetUrl = new URL(dstUrl); 44 | 45 | // 如果目标URL不支持WebSocket协议,返回错误响应 46 | if (!/^wss?:\/\//i.test(dstUrl)) { 47 | return new Response("Target does not support WebSocket", { status: 400 }); 48 | } 49 | 50 | // 通过SOCKS5代理连接 51 | const socket = await this.socks5Connect( 52 | 2, // domain name 53 | targetUrl.hostname, 54 | Number(targetUrl.port) || (targetUrl.protocol === "wss:" ? 443 : 80) 55 | ); 56 | 57 | // 生成WebSocket握手所需的密钥 58 | const key = this.generateWebSocketKey(); 59 | 60 | // 清理头部信息 61 | const cleanedHeaders = this.filterHeaders(req.headers); 62 | 63 | // 构建握手所需的HTTP头部 64 | cleanedHeaders.set('Host', targetUrl.hostname); 65 | cleanedHeaders.set('Connection', 'Upgrade'); 66 | cleanedHeaders.set('Upgrade', 'websocket'); 67 | cleanedHeaders.set('Sec-WebSocket-Version', '13'); 68 | cleanedHeaders.set('Sec-WebSocket-Key', key); 69 | 70 | // 组装WebSocket握手的HTTP请求数据 71 | const handshakeReq = 72 | `GET ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 73 | Array.from(cleanedHeaders.entries()) 74 | .map(([k, v]) => `${k}: ${v}`) 75 | .join('\r\n') + 76 | '\r\n\r\n'; 77 | 78 | this.log("Sending WebSocket handshake request", handshakeReq); 79 | const writer = socket.writable.getWriter(); 80 | await writer.write(this.encoder.encode(handshakeReq)); 81 | 82 | const reader = socket.readable.getReader(); 83 | const handshakeResp = await this.readUntilDoubleCRLF(reader); 84 | this.log("Received handshake response", handshakeResp); 85 | 86 | // 验证握手响应是否表明101 Switching Protocols状态 87 | if ( 88 | !handshakeResp.includes("101") || 89 | !handshakeResp.includes("Switching Protocols") 90 | ) { 91 | throw new Error("WebSocket handshake failed: " + handshakeResp); 92 | } 93 | 94 | // 创建内部WebSocketPair 95 | const webSocketPair = new WebSocketPair(); 96 | const client = webSocketPair[0]; 97 | const server = webSocketPair[1]; 98 | client.accept(); 99 | 100 | // 在客户端和远程套接字之间建立双向帧中继 101 | this.relayWebSocketFrames(client, socket, writer, reader); 102 | return new Response(null, { status: 101, webSocket: server }); 103 | } 104 | 105 | /** 106 | * 连接HTTP目标服务器 107 | * @param {Request} req - 请求对象 108 | * @param {string} dstUrl - 目标URL 109 | * @returns {Promise} 响应对象 110 | */ 111 | async connectHttp(req, dstUrl) { 112 | const targetUrl = new URL(dstUrl); 113 | 114 | // 清理头部信息 115 | const cleanedHeaders = this.filterHeaders(req.headers); 116 | 117 | // 对于标准HTTP请求:设置必需的头部(如Host并禁用压缩) 118 | cleanedHeaders.set("Host", targetUrl.hostname); 119 | cleanedHeaders.set("accept-encoding", "identity"); 120 | 121 | try { 122 | // 通过SOCKS5代理连接 123 | const socket = await this.socks5Connect( 124 | 2, // domain name 125 | targetUrl.hostname, 126 | Number(targetUrl.port) || (targetUrl.protocol === "https:" ? 443 : 80) 127 | ); 128 | 129 | const writer = socket.writable.getWriter(); 130 | 131 | // 构建请求行和头部 132 | const requestLine = 133 | `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 134 | Array.from(cleanedHeaders.entries()) 135 | .map(([k, v]) => `${k}: ${v}`) 136 | .join("\r\n") + 137 | "\r\n\r\n"; 138 | 139 | this.log("Sending request", requestLine); 140 | await writer.write(this.encoder.encode(requestLine)); 141 | 142 | // 如果有请求体,将其转发到目标服务器 143 | if (req.body) { 144 | this.log("Forwarding request body"); 145 | const reader = req.body.getReader(); 146 | while (true) { 147 | const { done, value } = await reader.read(); 148 | if (done) break; 149 | await writer.write(value); 150 | } 151 | } 152 | 153 | // 解析并返回目标服务器的响应 154 | return await this.parseResponse(socket.readable.getReader()); 155 | } catch (error) { 156 | // 使用统一的错误处理方法 157 | return this.handleError(error, "SOCKS5 connection"); 158 | } 159 | } 160 | 161 | /** 162 | * 通过SOCKS5代理连接 163 | * @param {number} addressType - 地址类型 164 | * @param {string} addressRemote - 远程地址 165 | * @param {number} portRemote - 远程端口 166 | * @returns {Promise} Socket对象 167 | */ 168 | async socks5Connect(addressType, addressRemote, portRemote) { 169 | const { username, password, hostname, port } = this.parsedSocks5Address; 170 | // Connect to the SOCKS server 171 | const socket = connect({ 172 | hostname, 173 | port, 174 | }); 175 | 176 | // Request head format (Worker -> Socks Server): 177 | // +----+----------+----------+ 178 | // |VER | NMETHODS | METHODS | 179 | // +----+----------+----------+ 180 | // | 1 | 1 | 1 to 255 | 181 | // +----+----------+----------+ 182 | 183 | // https://en.wikipedia.org/wiki/SOCKS#SOCKS5 184 | // For METHODS: 185 | // 0x00 NO AUTHENTICATION REQUIRED 186 | // 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929 187 | const socksGreeting = new Uint8Array([5, 2, 0, 2]); 188 | 189 | const writer = socket.writable.getWriter(); 190 | 191 | await writer.write(socksGreeting); 192 | this.log('sent socks greeting'); 193 | 194 | const reader = socket.readable.getReader(); 195 | const encoder = new TextEncoder(); 196 | let res = (await reader.read()).value; 197 | // Response format (Socks Server -> Worker): 198 | // +----+--------+ 199 | // |VER | METHOD | 200 | // +----+--------+ 201 | // | 1 | 1 | 202 | // +----+--------+ 203 | if (res[0] !== 0x05) { 204 | this.log(`socks server version error: ${res[0]} expected: 5`); 205 | throw new Error(`socks server version error: ${res[0]} expected: 5`); 206 | } 207 | if (res[1] === 0xff) { 208 | this.log("no acceptable methods"); 209 | throw new Error("no acceptable methods"); 210 | } 211 | 212 | // if return 0x0502 213 | if (res[1] === 0x02) { 214 | this.log("socks server needs auth"); 215 | if (!username || !password) { 216 | this.log("please provide username/password"); 217 | throw new Error("please provide username/password"); 218 | } 219 | // +----+------+----------+------+----------+ 220 | // |VER | ULEN | UNAME | PLEN | PASSWD | 221 | // +----+------+----------+------+----------+ 222 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 223 | // +----+------+----------+------+----------+ 224 | const authRequest = new Uint8Array([ 225 | 1, 226 | username.length, 227 | ...encoder.encode(username), 228 | password.length, 229 | ...encoder.encode(password) 230 | ]); 231 | await writer.write(authRequest); 232 | res = (await reader.read()).value; 233 | // expected 0x0100 234 | if (res[0] !== 0x01 || res[1] !== 0x00) { 235 | this.log("fail to auth socks server"); 236 | throw new Error("fail to auth socks server"); 237 | } 238 | } 239 | 240 | // Request data format (Worker -> Socks Server): 241 | // +----+-----+-------+------+----------+----------+ 242 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 243 | // +----+-----+-------+------+----------+----------+ 244 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 245 | // +----+-----+-------+------+----------+----------+ 246 | // ATYP: address type of following address 247 | // 0x01: IPv4 address 248 | // 0x03: Domain name 249 | // 0x04: IPv6 address 250 | // DST.ADDR: desired destination address 251 | // DST.PORT: desired destination port in network octet order 252 | 253 | // addressType 254 | // 1--> ipv4 addressLength =4 255 | // 2--> domain name 256 | // 3--> ipv6 addressLength =16 257 | let DSTADDR; // DSTADDR = ATYP + DST.ADDR 258 | switch (addressType) { 259 | case 1: 260 | DSTADDR = new Uint8Array( 261 | [1, ...addressRemote.split('.').map(Number)] 262 | ); 263 | break; 264 | case 2: 265 | DSTADDR = new Uint8Array( 266 | [3, addressRemote.length, ...encoder.encode(addressRemote)] 267 | ); 268 | break; 269 | case 3: 270 | DSTADDR = new Uint8Array( 271 | [4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])] 272 | ); 273 | break; 274 | default: 275 | this.log(`invalid addressType is ${addressType}`); 276 | throw new Error(`invalid addressType is ${addressType}`); 277 | } 278 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); 279 | await writer.write(socksRequest); 280 | this.log('sent socks request'); 281 | 282 | res = (await reader.read()).value; 283 | // Response format (Socks Server -> Worker): 284 | // +----+-----+-------+------+----------+----------+ 285 | // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 286 | // +----+-----+-------+------+----------+----------+ 287 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 288 | // +----+-----+-------+------+----------+----------+ 289 | if (res[1] === 0x00) { 290 | this.log("socks connection opened"); 291 | } else { 292 | this.log("fail to open socks connection"); 293 | throw new Error("fail to open socks connection"); 294 | } 295 | writer.releaseLock(); 296 | reader.releaseLock(); 297 | return socket; 298 | } 299 | 300 | /** 301 | * 解析SOCKS5地址 302 | * @param {string} address - SOCKS5地址 303 | * @returns {object} 解析后的地址信息 304 | */ 305 | parseSocks5Address(address) { 306 | let [latter, former] = address.split("@").reverse(); 307 | let username, password, hostname, port; 308 | if (former) { 309 | const formers = former.split(":"); 310 | if (formers.length !== 2) { 311 | throw new Error('Invalid SOCKS address format'); 312 | } 313 | [username, password] = formers; 314 | } 315 | const latters = latter.split(":"); 316 | port = Number(latters.pop()); 317 | if (isNaN(port)) { 318 | throw new Error('Invalid SOCKS address format'); 319 | } 320 | hostname = latters.join(":"); 321 | const regex = /^\[.*\]$/; 322 | if (hostname.includes(":") && !regex.test(hostname)) { 323 | throw new Error('Invalid SOCKS address format'); 324 | } 325 | return { 326 | username, 327 | password, 328 | hostname, 329 | port, 330 | } 331 | } 332 | } -------------------------------------------------------------------------------- /src/proxies/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础代理类 3 | * 所有代理策略都应继承此类 4 | */ 5 | export class BaseProxy { 6 | /** 7 | * 构造函数 8 | * @param {object} config - 配置对象 9 | */ 10 | constructor(config) { 11 | this.config = config; 12 | this.encoder = new TextEncoder(); 13 | this.decoder = new TextDecoder(); 14 | 15 | // 定义调试日志输出函数 16 | this.log = config.DEBUG_MODE 17 | ? (message, data = "") => console.log(`[DEBUG] ${message}`, data) 18 | : () => {}; 19 | } 20 | 21 | /** 22 | * 连接目标服务器 23 | * @param {Request} req - 请求对象 24 | * @param {string} dstUrl - 目标URL 25 | * @returns {Promise} 响应对象 26 | */ 27 | async connect(req, dstUrl) { 28 | throw new Error("connect method must be implemented by subclass"); 29 | } 30 | 31 | /** 32 | * 连接WebSocket目标服务器 33 | * @param {Request} req - 请求对象 34 | * @param {string} dstUrl - 目标URL 35 | * @returns {Promise} 响应对象 36 | */ 37 | async connectWebSocket(req, dstUrl) { 38 | throw new Error("connectWebSocket method must be implemented by subclass"); 39 | } 40 | 41 | /** 42 | * 连接HTTP目标服务器 43 | * @param {Request} req - 请求对象 44 | * @param {string} dstUrl - 目标URL 45 | * @returns {Promise} 响应对象 46 | */ 47 | async connectHttp(req, dstUrl) { 48 | throw new Error("connectHttp method must be implemented by subclass"); 49 | } 50 | 51 | /** 52 | * 处理DNS查询请求 53 | * @param {Request} req - 请求对象 54 | * @returns {Promise} 响应对象 55 | */ 56 | async handleDnsQuery(req) { 57 | // 默认实现 58 | return new Response("DNS query handling not implemented for this proxy type", { status: 501 }); 59 | } 60 | 61 | /** 62 | * 错误处理方法 63 | * @param {Error} error - 错误对象 64 | * @param {string} context - 错误上下文描述 65 | * @param {number} status - HTTP状态码 66 | * @returns {Response} 错误响应 67 | */ 68 | handleError(error, context, status = 500) { 69 | this.log(`${context} failed`, error.message); 70 | return new Response(`Error ${context.toLowerCase()}: ${error.message}`, { status }); 71 | } 72 | 73 | /** 74 | * 检查是否为Cloudflare网络限制错误 75 | * @param {Error} error - 错误对象 76 | * @returns {boolean} 是否为Cloudflare网络限制错误 77 | */ 78 | isCloudflareNetworkError(error) { 79 | // 默认实现 80 | return false; 81 | } 82 | 83 | /** 84 | * 通用的HTTP代理连接方法 85 | * @param {Request} req - 请求对象 86 | * @param {string} dstUrl - 目标URL 87 | * @param {string} proxyUrl - 代理URL 88 | * @param {string} proxyType - 代理类型(用于日志) 89 | * @returns {Promise} 响应对象 90 | */ 91 | async connectHttpViaProxy(req, dstUrl, proxyUrl, proxyType) { 92 | const targetUrl = new URL(dstUrl); 93 | const proxyUrlObj = new URL(proxyUrl); 94 | proxyUrlObj.searchParams.set('target', dstUrl); 95 | 96 | // 清理Cloudflare泄露隐私的头部信息 97 | const cleanedHeaders = this.filterHeaders(req.headers); 98 | 99 | // 设置必需的头部 100 | cleanedHeaders.set("Host", proxyUrlObj.hostname); 101 | 102 | try { 103 | // 使用代理进行连接 104 | const fetchRequest = new Request(proxyUrlObj.toString(), { 105 | method: req.method, 106 | headers: cleanedHeaders, 107 | body: req.body, 108 | }); 109 | 110 | this.log(`Using ${proxyType} proxy to connect to`, dstUrl); 111 | return await fetch(fetchRequest); 112 | } catch (error) { 113 | // 使用统一的错误处理方法 114 | return this.handleError(error, `${proxyType} proxy connection`); 115 | } 116 | } 117 | 118 | /** 119 | * 过滤HTTP头 120 | * @param {Headers} headers - HTTP头 121 | * @returns {Headers} 过滤后的HTTP头 122 | */ 123 | filterHeaders(headers) { 124 | // 过滤不应转发的HTTP头(忽略以下头部:host、accept-encoding、cf-*、cdn-*、referer、referrer) 125 | const HEADER_FILTER_RE = /^(host|accept-encoding|cf-|cdn-|referer|referrer)/i; 126 | const cleanedHeaders = new Headers(); 127 | 128 | for (const [k, v] of headers) { 129 | if (!HEADER_FILTER_RE.test(k)) { 130 | cleanedHeaders.set(k, v); 131 | } 132 | } 133 | 134 | return cleanedHeaders; 135 | } 136 | 137 | /** 138 | * 生成WebSocket握手所需的随机Sec-WebSocket-Key 139 | * @returns {string} WebSocket密钥 140 | */ 141 | generateWebSocketKey() { 142 | const bytes = new Uint8Array(16); 143 | crypto.getRandomValues(bytes); 144 | return btoa(String.fromCharCode(...bytes)); 145 | } 146 | 147 | /** 148 | * 在客户端和远程套接字之间双向中继WebSocket帧 149 | * @param {WebSocket} ws - WebSocket对象 150 | * @param {Socket} socket - Socket对象 151 | * @param {WritableStreamDefaultWriter} writer - 写入器 152 | * @param {ReadableStreamDefaultReader} reader - 读取器 153 | */ 154 | relayWebSocketFrames(ws, socket, writer, reader) { 155 | // 监听来自客户端的消息,将其打包成帧并发送到远程套接字 156 | ws.addEventListener("message", async (event) => { 157 | let payload; 158 | if (typeof event.data === "string") { 159 | payload = this.encoder.encode(event.data); 160 | } else if (event.data instanceof ArrayBuffer) { 161 | payload = new Uint8Array(event.data); 162 | } else { 163 | payload = event.data; 164 | } 165 | const frame = this.packTextFrame(payload); 166 | try { 167 | await writer.write(frame); 168 | } catch (e) { 169 | this.log("Remote write error", e); 170 | } 171 | }); 172 | 173 | // 异步中继从远程接收的WebSocket帧到客户端 174 | (async () => { 175 | const frameReader = new this.SocketFramesReader(reader, this); 176 | try { 177 | while (true) { 178 | const frame = await frameReader.nextFrame(); 179 | if (!frame) break; 180 | // 根据操作码处理数据帧 181 | switch (frame.opcode) { 182 | case 1: // 文本帧 183 | case 2: // 二进制帧 184 | ws.send(frame.payload); 185 | break; 186 | case 8: // 关闭帧 187 | this.log("Received Close frame, closing WebSocket"); 188 | ws.close(1000); 189 | return; 190 | default: 191 | this.log(`Received unknown frame type, Opcode: ${frame.opcode}`); 192 | } 193 | } 194 | } catch (e) { 195 | this.log("Error reading remote frame", e); 196 | } finally { 197 | ws.close(); 198 | writer.releaseLock(); 199 | socket.close(); 200 | } 201 | })(); 202 | 203 | // 当客户端WebSocket关闭时,也关闭远程套接字连接 204 | ws.addEventListener("close", () => socket.close()); 205 | } 206 | 207 | /** 208 | * 将文本消息打包成WebSocket帧 209 | * @param {Uint8Array} payload - 载荷 210 | * @returns {Uint8Array} 打包后的帧 211 | */ 212 | packTextFrame(payload) { 213 | const FIN_AND_OP = 0x81; // FIN标志和文本帧操作码 214 | const maskBit = 0x80; // 掩码位(客户端发送的消息必须设置为1) 215 | const len = payload.length; 216 | let header; 217 | if (len < 126) { 218 | header = new Uint8Array(2); 219 | header[0] = FIN_AND_OP; 220 | header[1] = maskBit | len; 221 | } else if (len < 65536) { 222 | header = new Uint8Array(4); 223 | header[0] = FIN_AND_OP; 224 | header[1] = maskBit | 126; 225 | header[2] = (len >> 8) & 0xff; 226 | header[3] = len & 0xff; 227 | } else { 228 | throw new Error("Payload too large"); 229 | } 230 | // 生成4字节随机掩码 231 | const mask = new Uint8Array(4); 232 | crypto.getRandomValues(mask); 233 | const maskedPayload = new Uint8Array(len); 234 | // 对载荷应用掩码 235 | for (let i = 0; i < len; i++) { 236 | maskedPayload[i] = payload[i] ^ mask[i % 4]; 237 | } 238 | // 连接帧头、掩码和掩码后的载荷 239 | return this.concatUint8Arrays(header, mask, maskedPayload); 240 | } 241 | 242 | /** 243 | * 用于解析和重组WebSocket帧的类,支持分片消息 244 | */ 245 | SocketFramesReader = class { 246 | /** 247 | * 构造函数 248 | * @param {ReadableStreamDefaultReader} reader - 读取器 249 | * @param {BaseProxy} parent - 父类实例 250 | */ 251 | constructor(reader, parent) { 252 | this.reader = reader; 253 | this.parent = parent; 254 | this.buffer = new Uint8Array(); 255 | this.fragmentedPayload = null; 256 | this.fragmentedOpcode = null; 257 | } 258 | 259 | /** 260 | * 确保缓冲区有足够的字节用于解析 261 | * @param {number} length - 长度 262 | * @returns {Promise} 是否有足够的字节 263 | */ 264 | async ensureBuffer(length) { 265 | while (this.buffer.length < length) { 266 | const { value, done } = await this.reader.read(); 267 | if (done) return false; 268 | this.buffer = this.parent.concatUint8Arrays(this.buffer, value); 269 | } 270 | return true; 271 | } 272 | 273 | /** 274 | * 解析下一个WebSocket帧并处理分片 275 | * @returns {Promise} 帧对象 276 | */ 277 | async nextFrame() { 278 | while (true) { 279 | if (!(await this.ensureBuffer(2))) return null; 280 | const first = this.buffer[0], 281 | second = this.buffer[1], 282 | fin = (first >> 7) & 1, 283 | opcode = first & 0x0f, 284 | isMasked = (second >> 7) & 1; 285 | let payloadLen = second & 0x7f, 286 | offset = 2; 287 | // 如果载荷长度为126,解析下两个字节获取实际长度 288 | if (payloadLen === 126) { 289 | if (!(await this.ensureBuffer(offset + 2))) return null; 290 | payloadLen = (this.buffer[offset] << 8) | this.buffer[offset + 1]; 291 | offset += 2; 292 | } else if (payloadLen === 127) { 293 | throw new Error("127 length mode is not supported"); 294 | } 295 | let mask; 296 | if (isMasked) { 297 | if (!(await this.ensureBuffer(offset + 4))) return null; 298 | mask = this.buffer.slice(offset, offset + 4); 299 | offset += 4; 300 | } 301 | if (!(await this.ensureBuffer(offset + payloadLen))) return null; 302 | let payload = this.buffer.slice(offset, offset + payloadLen); 303 | if (isMasked && mask) { 304 | for (let i = 0; i < payload.length; i++) { 305 | payload[i] ^= mask[i % 4]; 306 | } 307 | } 308 | // 从缓冲区中移除已处理的字节 309 | this.buffer = this.buffer.slice(offset + payloadLen); 310 | if (opcode === 0) { 311 | if (this.fragmentedPayload === null) 312 | throw new Error("Received continuation frame without initiation"); 313 | this.fragmentedPayload = this.parent.concatUint8Arrays(this.fragmentedPayload, payload); 314 | if (fin) { 315 | const completePayload = this.fragmentedPayload; 316 | const completeOpcode = this.fragmentedOpcode; 317 | this.fragmentedPayload = this.fragmentedOpcode = null; 318 | return { fin: true, opcode: completeOpcode, payload: completePayload }; 319 | } 320 | } else { 321 | // 如果有分片数据但当前帧不是延续帧,重置分片状态 322 | if (!fin) { 323 | this.fragmentedPayload = payload; 324 | this.fragmentedOpcode = opcode; 325 | continue; 326 | } else { 327 | if (this.fragmentedPayload) { 328 | this.fragmentedPayload = this.fragmentedOpcode = null; 329 | } 330 | return { fin, opcode, payload }; 331 | } 332 | } 333 | } 334 | } 335 | }; 336 | 337 | /** 338 | * 连接多个Uint8Array 339 | * @param {...Uint8Array} arrays - 要连接的数组 340 | * @returns {Uint8Array} 连接后的数组 341 | */ 342 | concatUint8Arrays(...arrays) { 343 | const total = arrays.reduce((sum, arr) => sum + arr.length, 0); 344 | const result = new Uint8Array(total); 345 | let offset = 0; 346 | for (const arr of arrays) { 347 | result.set(arr, offset); 348 | offset += arr.length; 349 | } 350 | return result; 351 | } 352 | 353 | /** 354 | * 解析HTTP响应头 355 | * @param {Uint8Array} buff - 缓冲区 356 | * @returns {object|null} 解析结果 357 | */ 358 | parseHttpHeaders(buff) { 359 | const text = this.decoder.decode(buff); 360 | // 查找由"\r\n\r\n"指示的HTTP头部结束标志 361 | const headerEnd = text.indexOf("\r\n\r\n"); 362 | if (headerEnd === -1) return null; 363 | const headerSection = text.slice(0, headerEnd).split("\r\n"); 364 | const statusLine = headerSection[0]; 365 | // 匹配HTTP状态行 366 | const statusMatch = statusLine.match(/HTTP\/1\.[01] (\d+) (.*)/); 367 | if (!statusMatch) throw new Error(`Invalid status line: ${statusLine}`); 368 | const headers = new Headers(); 369 | // 解析响应头 370 | for (let i = 1; i < headerSection.length; i++) { 371 | const line = headerSection[i]; 372 | const idx = line.indexOf(": "); 373 | if (idx !== -1) { 374 | headers.append(line.slice(0, idx), line.slice(idx + 2)); 375 | } 376 | } 377 | return { status: Number(statusMatch[1]), statusText: statusMatch[2], headers, headerEnd }; 378 | } 379 | 380 | /** 381 | * 读取直到双CRLF 382 | * @param {ReadableStreamDefaultReader} reader - 读取器 383 | * @returns {Promise} 读取的文本 384 | */ 385 | async readUntilDoubleCRLF(reader) { 386 | let respText = ""; 387 | while (true) { 388 | const { value, done } = await reader.read(); 389 | if (value) { 390 | respText += this.decoder.decode(value, { stream: true }); 391 | if (respText.includes("\r\n\r\n")) break; 392 | } 393 | if (done) break; 394 | } 395 | return respText; 396 | } 397 | 398 | /** 399 | * 解析完整的HTTP响应 400 | * @param {ReadableStreamDefaultReader} reader - 读取器 401 | * @returns {Promise} 响应对象 402 | */ 403 | async parseResponse(reader) { 404 | let buff = new Uint8Array(); 405 | while (true) { 406 | const { value, done } = await reader.read(); 407 | if (value) { 408 | buff = this.concatUint8Arrays(buff, value); 409 | const parsed = this.parseHttpHeaders(buff); 410 | if (parsed) { 411 | const { status, statusText, headers, headerEnd } = parsed; 412 | const isChunked = headers.get("transfer-encoding")?.includes("chunked"); 413 | const contentLength = parseInt(headers.get("content-length") || "0", 10); 414 | const data = buff.slice(headerEnd + 4); 415 | // 通过ReadableStream分发响应体数据 416 | // 保存this上下文 417 | const self = this; 418 | return new Response( 419 | new ReadableStream({ 420 | start: async (ctrl) => { 421 | try { 422 | if (isChunked) { 423 | console.log("Using chunked transfer mode"); 424 | // 分块传输模式:按顺序读取并入队每个块 425 | for await (const chunk of self.readChunks(reader, data)) { 426 | ctrl.enqueue(chunk); 427 | } 428 | } else { 429 | console.log("Using fixed-length transfer mode, contentLength: " + contentLength); 430 | let received = data.length; 431 | if (data.length) ctrl.enqueue(data); 432 | // 固定长度模式:根据content-length读取指定字节数 433 | while (received < contentLength) { 434 | const { value, done } = await reader.read(); 435 | if (done) break; 436 | received += value.length; 437 | ctrl.enqueue(value); 438 | } 439 | } 440 | ctrl.close(); 441 | } catch (err) { 442 | console.log("Error parsing response", err); 443 | ctrl.error(err); 444 | } 445 | }, 446 | }), 447 | { status, statusText, headers } 448 | ); 449 | } 450 | } 451 | if (done) break; 452 | } 453 | throw new Error("Unable to parse response headers"); 454 | } 455 | 456 | /** 457 | * 异步生成器:读取分块HTTP响应数据并按顺序产出每个数据块 458 | * @param {ReadableStreamDefaultReader} reader - 读取器 459 | * @param {Uint8Array} buff - 缓冲区 460 | * @returns {AsyncGenerator} 数据块生成器 461 | */ 462 | async *readChunks(reader, buff = new Uint8Array()) { 463 | while (true) { 464 | // 在现有缓冲区中查找CRLF分隔符的位置 465 | let pos = -1; 466 | for (let i = 0; i < buff.length - 1; i++) { 467 | if (buff[i] === 13 && buff[i + 1] === 10) { 468 | pos = i; 469 | break; 470 | } 471 | } 472 | // 如果未找到,继续读取更多数据来填充缓冲区 473 | if (pos === -1) { 474 | const { value, done } = await reader.read(); 475 | if (done) break; 476 | buff = this.concatUint8Arrays(buff, value); 477 | continue; 478 | } 479 | // 解析块大小(十六进制格式) 480 | const sizeStr = this.decoder.decode(buff.slice(0, pos)); 481 | const size = parseInt(sizeStr, 16); 482 | this.log("Read chunk size", size); 483 | // 大小为0表示块结束 484 | if (!size) break; 485 | // 从缓冲区中移除已解析的大小部分和后续的CRLF 486 | buff = buff.slice(pos + 2); 487 | // 确保缓冲区包含完整的块(包括尾部的CRLF) 488 | while (buff.length < size + 2) { 489 | const { value, done } = await reader.read(); 490 | if (done) throw new Error("Unexpected EOF in chunked encoding"); 491 | buff = this.concatUint8Arrays(buff, value); 492 | } 493 | // 产出块数据(不包括尾部的CRLF) 494 | yield buff.slice(0, size); 495 | buff = buff.slice(size + 2); 496 | } 497 | } 498 | } -------------------------------------------------------------------------------- /AIGatewayWithProxyIP半成品.js: -------------------------------------------------------------------------------- 1 | import { connect } from "cloudflare:sockets"; 2 | /** 3 | * AI Gateway with Proxy IP Fallback 4 | * 功能:随机UA,随机Accept-Language,dns解析,主连接使用socket直连,fallback使用cf反代ip(采取SNI PROXY的方式) 5 | * SNI PROXY:以原始域名作为SNI 进行TLS握手,但将IP地址改为ProxyIP 6 | * 轮询proxyIP的方案:使用dns解析域名的A记录,随机选取一个IP作为ProxyIP 7 | * 暂时无法使用,遇到了暂时无法解决的问题(分离 TCP 连接和 TLS 握手失败,未找到长期稳定可以获取,更新ProxyIP的方法) 8 | */ 9 | // 全局配置 10 | const DEFAULT_CONFIG = { 11 | AUTH_TOKEN: "defaulttoken", 12 | DEFAULT_DST_URL: "https://httpbin.org/get", 13 | DEBUG_MODE: true, 14 | ENABLE_UA_RANDOMIZATION: true, 15 | ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION: false, // 随机 Accept-Language 16 | PROXY_DOMAINS: [""], // CF反代IP域名列表 17 | }; 18 | 19 | // 使用默认配置创建可更新的副本 20 | let CONFIG = { ...DEFAULT_CONFIG }; 21 | 22 | // 从环境变量更新配置 23 | function updateConfigFromEnv(env) { 24 | if (!env) return; 25 | for (const key of Object.keys(CONFIG)) { 26 | if (key in env) { 27 | if (typeof CONFIG[key] === 'boolean') { 28 | CONFIG[key] = env[key] === 'true'; 29 | } else if (typeof CONFIG[key] === 'number') { 30 | CONFIG[key] = Number(env[key]); 31 | } else if (key === 'PROXY_DOMAINS' && typeof env[key] === 'string') { 32 | CONFIG[key] = env[key].split(',').map(d => d.trim()).filter(Boolean); 33 | } else { 34 | CONFIG[key] = env[key]; 35 | } 36 | } 37 | } 38 | } 39 | 40 | // 文本编码器/解码器 41 | const encoder = new TextEncoder(); 42 | const decoder = new TextDecoder(); 43 | 44 | // 忽略的请求头正则 45 | const HEADER_FILTER_RE = /^(host|cf-|cdn-|referer|referrer)/i; 46 | 47 | // 日志函数 48 | let log = () => {}; 49 | 50 | // 管理器实例 51 | let userAgentManager; 52 | 53 | /** 54 | * User-Agent 管理器,存储一些常用的UA供随机化使用 55 | */ 56 | class UserAgentManager { 57 | constructor() { 58 | this.userAgents = [ 59 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 60 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1', 61 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1', 62 | 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/117.0.0.0 Mobile Safari/537.36', 63 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 64 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0', 65 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 66 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15', 67 | 'Mozilla/5.0 (Linux; Android 13; SM-S908U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36', 68 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' 69 | ]; 70 | this.currentIndex = Math.floor(Math.random() * this.userAgents.length); 71 | 72 | // Accept-Language 值列表 73 | this.acceptLanguages = [ 74 | 'zh-CN,zh;q=0.9,en;q=0.8', 75 | 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 76 | 'zh-CN,zh;q=0.9', 77 | 'en-US,en;q=0.9', 78 | 'en-US,en;q=0.9,es;q=0.8', 79 | 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', 80 | 'en-GB,en;q=0.9', 81 | 'en-GB,en-US;q=0.9,en;q=0.8', 82 | 'en-GB,en;q=0.9,fr;q=0.8', 83 | 'en-SG,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', 84 | 'zh-CN,zh;q=0.9,en-SG;q=0.8,en;q=0.7', 85 | 'en-SG,en;q=0.9,ms;q=0.8' 86 | ]; 87 | } 88 | 89 | // 随机UA 90 | getRandomUserAgent() { 91 | if (!CONFIG.ENABLE_UA_RANDOMIZATION) return null; 92 | this.currentIndex = (this.currentIndex + 1 + Math.floor(Math.random() * 2)) % this.userAgents.length; 93 | return this.userAgents[this.currentIndex]; 94 | } 95 | 96 | // 兼容UA方法 97 | getCompatibleUserAgent(originalUA) { 98 | if (!CONFIG.ENABLE_UA_RANDOMIZATION || !originalUA) return null; 99 | const isWindows = /Windows/.test(originalUA); 100 | const isMac = /Macintosh/.test(originalUA); 101 | const isLinux = /Linux/.test(originalUA); 102 | const isAndroid = /Android/.test(originalUA); 103 | const isiPhone = /iPhone/.test(originalUA); 104 | const compatibleAgents = this.userAgents.filter(ua => { 105 | if (isWindows) return /Windows/.test(ua); 106 | if (isMac) return /Macintosh/.test(ua); 107 | if (isAndroid) return /Android/.test(ua); 108 | if (isiPhone) return /iPhone/.test(ua); 109 | if (isLinux) return /Linux/.test(ua); 110 | return true; 111 | }); 112 | return compatibleAgents[Math.floor(Math.random() * compatibleAgents.length)]; 113 | } 114 | 115 | // 随机Accept-Language 116 | getRandomAcceptLanguage() { 117 | if (!CONFIG.ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION) return null; 118 | return this.acceptLanguages[Math.floor(Math.random() * this.acceptLanguages.length)]; 119 | } 120 | } 121 | 122 | 123 | // 创建新连接的辅助函数 124 | async function createNewConnection(hostname, port, isSecure) { 125 | log(`创建新连接 ${hostname}:${port}`); 126 | 127 | return await connect( 128 | { hostname, port: Number(port) }, 129 | { 130 | secureTransport: isSecure ? "on" : "off", 131 | allowHalfOpen: true 132 | } 133 | ); 134 | } 135 | 136 | 137 | // 初始化管理器 138 | function initializeManagers() { 139 | if (!userAgentManager) userAgentManager = new UserAgentManager(); 140 | } 141 | 142 | // 连接Uint8Array 143 | function concatUint8Arrays(...arrays) { 144 | const total = arrays.reduce((sum, arr) => sum + arr.length, 0); 145 | const result = new Uint8Array(total); 146 | let offset = 0; 147 | for (const arr of arrays) { 148 | result.set(arr, offset); 149 | offset += arr.length; 150 | } 151 | return result; 152 | } 153 | 154 | // 解析HTTP头部 155 | function parseHttpHeaders(buff) { 156 | try { 157 | const text = decoder.decode(buff); 158 | const headerEnd = text.indexOf("\r\n\r\n"); 159 | if (headerEnd === -1) return null; 160 | 161 | const headerSection = text.slice(0, headerEnd).split("\r\n"); 162 | const statusLine = headerSection[0]; 163 | const statusMatch = statusLine.match(/HTTP\/1\.[01] (\d+) (.*)/); 164 | if (!statusMatch) throw new Error(`无效状态行: ${statusLine}`); 165 | 166 | const headers = new Headers(); 167 | for (let i = 1; i < headerSection.length; i++) { 168 | const line = headerSection[i]; 169 | const idx = line.indexOf(": "); 170 | if (idx !== -1) headers.append(line.slice(0, idx), line.slice(idx + 2)); 171 | } 172 | 173 | return { 174 | status: Number(statusMatch[1]), 175 | statusText: statusMatch[2], 176 | headers, 177 | headerEnd 178 | }; 179 | } catch (error) { 180 | log('解析HTTP头部错误:', error); 181 | throw error; 182 | } 183 | } 184 | 185 | // 读取直到双CRLF 186 | async function readUntilDoubleCRLF(reader) { 187 | let respText = ""; 188 | while (true) { 189 | const { value, done } = await reader.read(); 190 | if (done) break; 191 | respText += decoder.decode(value, { stream: true }); 192 | if (respText.includes("\r\n\r\n")) break; 193 | } 194 | return respText; 195 | } 196 | 197 | // 读取分块数据 198 | async function* readChunks(reader, buff = new Uint8Array()) { 199 | while (true) { 200 | let pos = -1; 201 | // 寻找分块大小行结束的 CRLF 202 | for (let i = 0; i < buff.length - 1; i++) { 203 | if (buff[i] === 13 && buff[i+1] === 10) { // CR LF 204 | pos = i; 205 | break; 206 | } 207 | } 208 | 209 | // 如果找不到,说明分块大小行不完整,需要从socket读取更多数据 210 | if (pos === -1) { 211 | const { value, done } = await reader.read(); 212 | if (done) break; // 流结束 213 | buff = concatUint8Arrays(buff, value); 214 | continue; 215 | } 216 | 217 | const sizeHex = decoder.decode(buff.slice(0, pos)); 218 | const size = parseInt(sizeHex, 16); 219 | 220 | // 如果大小为0,表示是最后一个分块,流结束 221 | if (isNaN(size) || size === 0) { 222 | log("读取到最后一个分块 (size=0),流结束"); 223 | break; 224 | } 225 | 226 | // 移除大小行和CRLF 227 | buff = buff.slice(pos + 2); 228 | 229 | // 循环读取直到获得完整的一个数据块 230 | while (buff.length < size + 2) { // +2 是为了数据块末尾的CRLF 231 | const { value, done } = await reader.read(); 232 | if (done) throw new Error("分块编码中意外结束"); 233 | buff = concatUint8Arrays(buff, value); 234 | } 235 | 236 | // 提取纯数据块 (payload) 237 | const chunkData = buff.slice(0, size); 238 | yield chunkData; 239 | 240 | // 移除已处理的数据块和它末尾的CRLF 241 | buff = buff.slice(size + 2); 242 | } 243 | } 244 | 245 | // 解析响应 246 | async function parseResponse(reader, targetHost, targetPort, socket) { 247 | let buff = new Uint8Array(); 248 | 249 | try { 250 | // 循环读取,直到解析出完整的HTTP头部 251 | while (true) { 252 | const { value, done } = await reader.read(); 253 | if (value) buff = concatUint8Arrays(buff, value); 254 | 255 | // 如果流结束但缓冲区为空,则退出 256 | if (done && !buff.length) { 257 | throw new Error("无法解析响应:流提前结束且无数据"); 258 | } 259 | 260 | const parsed = parseHttpHeaders(buff); 261 | if (parsed) { 262 | const { status, statusText, headers, headerEnd } = parsed; 263 | 264 | // 关键逻辑:检查是分块编码还是定长编码 265 | const isChunked = headers.get("transfer-encoding")?.toLowerCase().includes("chunked"); 266 | const contentLength = parseInt(headers.get("content-length") || "0", 10); 267 | 268 | // 提取HTTP头部之后的数据,这部分是响应体的开始 269 | const initialBodyData = buff.slice(headerEnd + 4); 270 | 271 | return new Response( 272 | new ReadableStream({ 273 | async start(ctrl) { 274 | try { 275 | if (isChunked) { 276 | // 如果是分块编码,使用 readChunks 生成器进行解析 277 | log("响应模式:分块编码 (Chunked)"); 278 | for await (const chunk of readChunks(reader, initialBodyData)) { 279 | ctrl.enqueue(chunk); 280 | } 281 | } else { 282 | // 如果是定长编码,按长度读取 283 | log(`响应模式:定长 (Content-Length: ${contentLength})`); 284 | let receivedLength = initialBodyData.length; 285 | if (initialBodyData.length > 0) { 286 | ctrl.enqueue(initialBodyData); 287 | } 288 | 289 | // 循环读取直到满足 Content-Length 290 | while (receivedLength < contentLength) { 291 | const { value, done } = await reader.read(); 292 | if (done) { 293 | log("警告:流在达到Content-Length之前结束"); 294 | break; 295 | } 296 | receivedLength += value.length; 297 | ctrl.enqueue(value); 298 | } 299 | } 300 | 301 | // 所有数据处理完毕 302 | ctrl.close(); 303 | } catch (err) { 304 | log("流式响应处理错误", err); 305 | ctrl.error(err); 306 | } finally { 307 | // 确保socket被关闭 308 | if (socket && !socket.closed) { 309 | socket.close(); 310 | } 311 | } 312 | }, 313 | cancel() { 314 | log("流被客户端取消"); 315 | if (socket && !socket.closed) { 316 | socket.close(); 317 | } 318 | }, 319 | }), 320 | { status, statusText, headers } 321 | ); 322 | } 323 | // 如果流结束了还没解析出头部,抛出错误 324 | if (done) { 325 | throw new Error("无法解析响应头:流已结束"); 326 | } 327 | } 328 | } catch (error) { 329 | log("解析响应时发生错误", error); 330 | if (socket && !socket.closed) { 331 | socket.close(); 332 | } 333 | // 重新抛出错误,让上层捕获 334 | throw error; 335 | } 336 | } 337 | 338 | 339 | /** 340 | * 为给定的域名构建一个DNS查询消息。 341 | * @param {string} domain 要查询的域名。 342 | * @returns {Uint8Array} DNS查询消息。 343 | */ 344 | function buildDnsQuery(domain) { 345 | const header = new Uint8Array([ 346 | Math.floor(Math.random() * 256), Math.floor(Math.random() * 256), // 事务ID 347 | 0x01, 0x00, 348 | 0x00, 0x01, 349 | 0x00, 0x00, 350 | 0x00, 0x00, 351 | 0x00, 0x00, 352 | ]); 353 | 354 | const labels = domain.split('.'); 355 | const question = new Uint8Array(domain.length + 2 + 4); 356 | let offset = 0; 357 | for (const label of labels) { 358 | question[offset++] = label.length; 359 | for (let i = 0; i < label.length; i++) { 360 | question[offset++] = label.charCodeAt(i); 361 | } 362 | } 363 | question[offset++] = 0; // 域名结束 364 | 365 | // 查询类型 (A) 和类 (IN) 366 | question[offset++] = 0x00; 367 | question[offset++] = 0x01; // 类型 A 368 | question[offset++] = 0x00; 369 | question[offset++] = 0x01; // 类 IN 370 | 371 | return concatUint8Arrays(header, question.slice(0, offset)); 372 | } 373 | 374 | /** 375 | * 从二进制DNS响应中解析IP地址。 376 | * @param {Uint8Array} buffer 包含DNS响应的缓冲区。 377 | * @returns {string[]} 从A记录中提取的IP地址数组。 378 | */ 379 | function parseDnsResponse(buffer) { 380 | const dataView = new DataView(buffer.buffer); 381 | const answerCount = dataView.getUint16(6); 382 | let offset = 12; // 跳过头部 383 | 384 | // 跳过问题部分 385 | while (buffer[offset] !== 0) { 386 | if (offset > buffer.length) return []; // 防止死循环 387 | offset += buffer[offset] + 1; 388 | } 389 | offset += 5; // 跳过问题末尾的0字节和类型/类 390 | 391 | const addresses = []; 392 | for (let i = 0; i < answerCount; i++) { 393 | if (offset + 12 > buffer.length) break; 394 | 395 | // 跳过名称(通常是指针,占2字节) 396 | offset += 2; 397 | 398 | const type = dataView.getUint16(offset); 399 | offset += 2; // 跳过类型 400 | offset += 6; // 跳过类和TTL 401 | const rdLength = dataView.getUint16(offset); 402 | offset += 2; // 跳过rdLength 403 | 404 | if (type === 1 && rdLength === 4) { // A记录 405 | addresses.push(`${buffer[offset]}.${buffer[offset+1]}.${buffer[offset+2]}.${buffer[offset+3]}`); 406 | } 407 | offset += rdLength; 408 | } 409 | 410 | return addresses; 411 | } 412 | 413 | /** 414 | * 查询域名的A记录,并从结果中随机返回一个IP。 415 | * @param {string} domain 要查询的域名。 416 | * @returns {Promise} 一个随机的IP地址,如果找不到则返回null。 417 | */ 418 | async function resolveDomainRandomIP(domain) { 419 | log(`为域执行二进制DNS查询: ${domain}`); 420 | const query = buildDnsQuery(domain); 421 | try { 422 | const response = await fetch('https://1.1.1.1/dns-query', { 423 | method: 'POST', 424 | headers: { 'content-type': 'application/dns-message' }, 425 | body: query, 426 | }); 427 | if (!response.ok) { 428 | throw new Error(`DNS查询失败,状态为 ${response.status}`); 429 | } 430 | const addresses = parseDnsResponse(new Uint8Array(await response.arrayBuffer())); 431 | if (addresses.length === 0) { 432 | log(`未找到域 ${domain} 的A记录`); 433 | return null; 434 | } 435 | const randomIP = addresses[Math.floor(Math.random() * addresses.length)]; 436 | log(`为 ${domain} 解析到随机IP: ${randomIP}`); 437 | return randomIP; 438 | } catch (error) { 439 | log('二进制DNS解析错误:', error); 440 | throw error; 441 | } 442 | } 443 | 444 | /** 445 | * 辅助函数,通过socket发送HTTP请求 446 | * @param {string} hostname - 目标主机名或IP 447 | * @param {number} port - 目标端口 448 | * @param {boolean} isSecure - 是否使用TLS 449 | * @param {Request} req - 原始请求对象 450 | * @param {Headers} headers - 清理和修改后的请求头 451 | * @param {URL} targetUrl - 目标URL对象 452 | * @returns {Promise} 453 | */ 454 | async function sendRequestViaSocket(hostname, port, isSecure, req, headers, targetUrl) { 455 | const socket = await createNewConnection(hostname, port, isSecure); 456 | try { 457 | const writer = socket.writable.getWriter(); 458 | const requestLine = `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 459 | Array.from(headers.entries()).map(([k, v]) => `${k}: ${v}`).join("\r\n") + "\r\n\r\n"; 460 | 461 | log(`通过 socket 发送请求到 ${hostname}:${port}`); 462 | await writer.write(encoder.encode(requestLine)); 463 | 464 | if (req.body) { 465 | for await (const chunk of req.body) { 466 | await writer.write(chunk); 467 | } 468 | } 469 | writer.releaseLock(); 470 | return await parseResponse(socket.readable.getReader(), hostname, port, socket); 471 | } catch (error) { 472 | if (!socket.closed) { 473 | socket.close(); 474 | } 475 | // 重新抛出错误,让调用者处理 476 | throw error; 477 | } 478 | } 479 | 480 | /** 481 | * 原生HTTP请求 482 | */ 483 | async function nativeFetch(req, dstUrl) { 484 | // 清理请求头和应用随机化 485 | const cleanedHeaders = new Headers(); 486 | for (const [k, v] of req.headers) { 487 | if (!HEADER_FILTER_RE.test(k)) cleanedHeaders.set(k, v); 488 | } 489 | 490 | const randomUA = userAgentManager.getCompatibleUserAgent(req.headers.get('user-agent')); 491 | if (randomUA) { 492 | cleanedHeaders.set('User-Agent', randomUA); 493 | log('使用User-Agent:', randomUA); 494 | } 495 | 496 | if (CONFIG.ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION) { 497 | const randomLang = userAgentManager.getRandomAcceptLanguage(); 498 | if (randomLang) { 499 | cleanedHeaders.set('Accept-Language', randomLang); 500 | log('使用Accept-Language:', randomLang); 501 | } 502 | } 503 | 504 | const targetUrl = new URL(dstUrl); 505 | const port = targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80); 506 | const isSecure = targetUrl.protocol === "https:"; 507 | 508 | // 设置主机头 509 | cleanedHeaders.set("Host", targetUrl.hostname); 510 | cleanedHeaders.set("Connection", "close"); 511 | 512 | // 克隆请求 513 | const reqForFallback = req.clone(); 514 | 515 | try { 516 | // 尝试直接连接 517 | log(`尝试直接连接到 ${targetUrl.hostname}:${port}`); 518 | return await sendRequestViaSocket(targetUrl.hostname, port, isSecure, req, cleanedHeaders, targetUrl); 519 | } catch (error) { 520 | log('直接 socket 连接失败,尝试 Fallback:', error.message); 521 | 522 | //Fallback 逻辑 523 | if (CONFIG.PROXY_DOMAINS && CONFIG.PROXY_DOMAINS.length > 0) { 524 | const randomDomain = CONFIG.PROXY_DOMAINS[Math.floor(Math.random() * CONFIG.PROXY_DOMAINS.length)]; 525 | const proxyIP = await resolveDomainRandomIP(randomDomain); 526 | 527 | if (proxyIP) { 528 | log(`使用代理IP ${proxyIP} (来自 ${randomDomain}) 进行 Fallback 连接`); 529 | try { 530 | // 使用克隆的请求进行 fallback 531 | return await sendRequestViaSocket(proxyIP, port, isSecure, reqForFallback, cleanedHeaders, targetUrl); 532 | } catch (proxyError) { 533 | log('ProxyIP连接失败:', proxyError.message); 534 | // 抛出原始错误以保持一致性 535 | throw error; 536 | } 537 | } else { 538 | log(`无法为 ${randomDomain} 解析IP,Fallback 失败`); 539 | } 540 | } 541 | 542 | // 如果没有 fallback 选项或 fallback 失败,重新抛出原始错误 543 | throw error; 544 | } 545 | } 546 | 547 | 548 | /** 549 | * 请求处理入口 550 | */ 551 | async function handleRequest(req, env) { 552 | // 克隆并更新配置(避免污染全局状态) 553 | CONFIG = { ...DEFAULT_CONFIG, ...env }; 554 | updateConfigFromEnv(env); 555 | 556 | // 初始化管理器 557 | initializeManagers(); 558 | 559 | // 设置日志 560 | log = CONFIG.DEBUG_MODE 561 | ? (message, data = "") => console.log(`[${new Date().toISOString()}] ${message}`, data) 562 | : () => {}; 563 | 564 | const url = new URL(req.url); 565 | 566 | // 路由处理 567 | try { 568 | const pathSegments = url.pathname.split('/').filter(Boolean); 569 | 570 | // 如果路径为空, 则请求默认目标地址 571 | if (pathSegments.length === 0) { 572 | log("无路径请求,转发至默认URL", CONFIG.DEFAULT_DST_URL); 573 | const dstUrl = CONFIG.DEFAULT_DST_URL + url.search; 574 | return await nativeFetch(req, dstUrl); 575 | } 576 | if (authToken === "defaulttoken") { 577 | const msg = "请修改默认AUTH_TOKEN,建议随机字符串10位以上"; 578 | log(msg); 579 | return new Response(msg, { status: 401 }); 580 | } 581 | 582 | const authToken = pathSegments[0]; 583 | const hasTargetUrl = pathSegments.length >= 2; 584 | 585 | // 如果鉴权令牌不匹配或缺少目标URL 586 | if (authToken !== CONFIG.AUTH_TOKEN || !hasTargetUrl) { 587 | const msg = "Invalid path. Expected `/{authtoken}/{target_url}`. please check authentictoken or targeturl"; 588 | log(msg, { authToken, hasTargetUrl }); 589 | return new Response(msg, { status: 400 }); 590 | } 591 | 592 | // 提取目标URL 593 | const authtokenPrefix = `/${authToken}/`; 594 | let targetUrl = url.pathname.substring(url.pathname.indexOf(authtokenPrefix) + authtokenPrefix.length); 595 | targetUrl = decodeURIComponent(targetUrl); 596 | 597 | // 验证URL协议 (http/https) 598 | if (!/^https?:\/\//i.test(targetUrl)) { 599 | const msg = "Invalid target URL. Protocol (http/https) is required."; 600 | log(msg, { targetUrl }); 601 | return new Response(msg, { status: 400 }); 602 | } 603 | 604 | const dstUrl = targetUrl + url.search; 605 | log("目标URL", dstUrl); 606 | return await nativeFetch(req, dstUrl); 607 | 608 | } catch (error) { 609 | log("请求处理失败", error); 610 | return new Response("Bad Gateway", { status: 502 }); 611 | } 612 | } 613 | 614 | // 导出Worker handlers 615 | export default { fetch: handleRequest }; 616 | export const onRequest = (ctx) => handleRequest(ctx.request, ctx.env); 617 | -------------------------------------------------------------------------------- /AIGatewayWithSocks.js: -------------------------------------------------------------------------------- 1 | import { connect } from "cloudflare:sockets"; 2 | 3 | // 全局配置 4 | const DEFAULT_CONFIG = { 5 | AUTH_TOKEN: "defaulttoken", // 默认鉴权令牌,必须更改 6 | DEFAULT_DST_URL: "https://httpbin.org/get", 7 | DEBUG_MODE: true, 8 | ENABLE_UA_RANDOMIZATION: true, 9 | ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION: false, // 随机 Accept-Language 10 | ENABLE_SOCKS5_FALLBACK: true, // 启用 Socks5 fallback 11 | }; 12 | 13 | // Socks5 API 14 | const SOCKS5_API_URLS = [ 15 | "https://api1.example.com/socks5", 16 | "https://api2.example.com/socks5", 17 | "https://api3.example.com/socks5" 18 | ]; 19 | 20 | // 主机请求方式配置集合 (key: host, value: 'nativeFetch' | 'socks5') 21 | const HOST_REQUEST_CONFIG = new Map([ 22 | ["api.openai.com", "socks5"], 23 | ["generativelanguage.googleapis.com", "nativeFetch"], 24 | ["api.anthropic.com", "socks5"], 25 | ["api.cohere.ai", "nativeFetch"], 26 | ["httpbin.org", "nativeFetch"], 27 | ]); 28 | 29 | // URL预设映射 (简化路径映射到完整URL) 30 | const URL_PRESETS = new Map([ 31 | ["gemini", "https://generativelanguage.googleapis.com"], 32 | ["openai", "https://api.openai.com"], 33 | ["anthropic", "https://api.anthropic.com"], 34 | ["cohere", "https://api.cohere.ai"], 35 | ["httpbin", "https://httpbin.org"], 36 | ]); 37 | let CONFIG = { ...DEFAULT_CONFIG }; 38 | 39 | function updateConfigFromEnv(env) { 40 | if (!env) return; 41 | for (const key of Object.keys(CONFIG)) { 42 | if (key in env) { 43 | if (typeof CONFIG[key] === 'boolean') CONFIG[key] = env[key] === 'true'; 44 | else if (typeof CONFIG[key] === 'number') CONFIG[key] = Number(env[key]); 45 | else CONFIG[key] = env[key]; 46 | } 47 | } 48 | } 49 | 50 | const encoder = new TextEncoder(); 51 | const decoder = new TextDecoder(); 52 | const HEADER_FILTER_RE = /^(host|cf-|cdn-|referer|referrer)/i; 53 | let log = () => {}; 54 | let userAgentManager; 55 | 56 | function getRequestMethodForHost(hostname) { 57 | return HOST_REQUEST_CONFIG.get(hostname) || 'nativeFetch'; 58 | } 59 | 60 | function resolveUrl(urlOrPreset) { 61 | return URL_PRESETS.get(urlOrPreset) || urlOrPreset; 62 | } 63 | 64 | class UserAgentManager { 65 | constructor() { 66 | this.userAgents = [ 67 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 68 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/123.0.6312.52 Mobile/15E148 Safari/604.1', 69 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Mobile/15E148 Safari/604.1', 70 | 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/117.0.0.0 Mobile Safari/537.36', 71 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 72 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0', 73 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 74 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15', 75 | 'Mozilla/5.0 (Linux; Android 13; SM-S908U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36', 76 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' 77 | ]; 78 | this.acceptLanguages = [ 79 | 'zh-CN,zh;q=0.9,en;q=0.8', 80 | 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7', 81 | 'zh-CN,zh;q=0.9', 82 | 'en-US,en;q=0.9', 83 | 'en-US,en;q=0.9,es;q=0.8', 84 | 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', 85 | 'en-GB,en;q=0.9', 86 | 'en-GB,en-US;q=0.9,en;q=0.8', 87 | 'en-GB,en;q=0.9,fr;q=0.8', 88 | 'en-SG,en;q=0.9,zh-CN;q=0.8,zh;q=0.7', 89 | 'zh-CN,zh;q=0.9,en-SG;q=0.8,en;q=0.7', 90 | 'en-SG,en;q=0.9,ms;q=0.8' 91 | ]; 92 | } 93 | getRandomUserAgent() { 94 | if (!CONFIG.ENABLE_UA_RANDOMIZATION) return null; 95 | return this.userAgents[Math.floor(Math.random() * this.userAgents.length)]; 96 | } 97 | getCompatibleUserAgent(originalUA) { 98 | if (!CONFIG.ENABLE_UA_RANDOMIZATION || !originalUA) return null; 99 | const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(originalUA); 100 | const compatibleAgents = this.userAgents.filter(ua => isMobile === /Android|iPhone|iPad|iPod|Mobile/i.test(ua)); 101 | return compatibleAgents.length > 0 ? compatibleAgents[Math.floor(Math.random() * compatibleAgents.length)] : this.getRandomUserAgent(); 102 | } 103 | getRandomAcceptLanguage() { 104 | if (!CONFIG.ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION) return null; 105 | return this.acceptLanguages[Math.floor(Math.random() * this.acceptLanguages.length)]; 106 | } 107 | } 108 | 109 | async function parseSocks5Proxy() { 110 | try { 111 | const randomApiUrl = SOCKS5_API_URLS[Math.floor(Math.random() * SOCKS5_API_URLS.length)]; 112 | const response = await fetch(randomApiUrl, { method: 'GET' }); 113 | if (!response.ok) throw new Error(`获取 Socks5 代理失败: ${response.status}`); 114 | const proxyData = await response.text(); 115 | if (!proxyData || proxyData.trim() === '') throw new Error('未获取到 Socks5 代理数据'); 116 | const proxyList = proxyData.trim().split('\n').filter(line => line.trim()); 117 | if (proxyList.length === 0) throw new Error('代理列表为空'); 118 | const selectedProxy = proxyList[Math.floor(Math.random() * proxyList.length)]; 119 | let proxyStr = selectedProxy.trim(); 120 | let username = null, password = null, host = null, port = null; 121 | if (proxyStr.startsWith('socks5://')) proxyStr = proxyStr.substring(9); 122 | if (proxyStr.includes('@')) { 123 | const [authPart, addressPart] = proxyStr.split('@'); 124 | if (authPart) [username, password] = authPart.split(':'); 125 | [host, port] = addressPart.split(':'); 126 | } else { 127 | [host, port] = proxyStr.split(':'); 128 | } 129 | port = parseInt(port); 130 | if (!host || !port || isNaN(port)) throw new Error(`代理格式不完整: ${selectedProxy}`); 131 | return { host, port, username, password, hasAuth: !!(username && password) }; 132 | } catch (error) { 133 | log('解析 Socks5 代理失败:', error.message); 134 | throw error; 135 | } 136 | } 137 | 138 | async function performSocks5Handshake(reader, writer, targetHost, targetPort, username, password) { 139 | const hasAuth = !!(username && password); 140 | await writer.write(hasAuth ? new Uint8Array([0x05, 0x01, 0x02]) : new Uint8Array([0x05, 0x01, 0x00])); 141 | const authResult = await reader.read(); 142 | if (authResult.done || authResult.value[0] !== 0x05) throw new Error('Socks5 版本不匹配或响应错误'); 143 | const selectedMethod = authResult.value[1]; 144 | if (hasAuth && selectedMethod !== 0x02) throw new Error('Socks5 服务器不支持用户名密码认证'); 145 | if (!hasAuth && selectedMethod !== 0x00) throw new Error('Socks5 服务器需要认证但未提供'); 146 | if (hasAuth) { 147 | const usernameBytes = encoder.encode(username); 148 | const passwordBytes = encoder.encode(password); 149 | const authData = new Uint8Array(3 + usernameBytes.length + passwordBytes.length); 150 | authData.set([0x01, usernameBytes.length], 0); 151 | authData.set(usernameBytes, 2); 152 | authData.set([passwordBytes.length], 2 + usernameBytes.length); 153 | authData.set(passwordBytes, 3 + usernameBytes.length); 154 | await writer.write(authData); 155 | const authResult2 = await reader.read(); 156 | if (authResult2.done || authResult2.value[1] !== 0x00) throw new Error('Socks5 用户名密码认证失败'); 157 | } 158 | const hostBytes = encoder.encode(targetHost); 159 | const connectRequest = new Uint8Array(7 + hostBytes.length); 160 | connectRequest.set([0x05, 0x01, 0x00, 0x03, hostBytes.length], 0); 161 | connectRequest.set(hostBytes, 5); 162 | connectRequest.set([(targetPort >> 8) & 0xFF, targetPort & 0xFF], 5 + hostBytes.length); 163 | await writer.write(connectRequest); 164 | const connectResult = await reader.read(); 165 | if (connectResult.done || connectResult.value[1] !== 0x00) throw new Error(`Socks5 连接请求失败: 错误码 ${connectResult.value[1]}`); 166 | log('Socks5 握手完成'); 167 | } 168 | 169 | async function fetchViaSocks5(req, dsturl) { 170 | const targetUrl = new URL(dsturl); 171 | try { 172 | const proxyConfig = await parseSocks5Proxy(); 173 | const headers = new Headers(req.headers); 174 | for (const key of req.headers.keys()) if (HEADER_FILTER_RE.test(key)) headers.delete(key); 175 | const randomUA = userAgentManager.getRandomUserAgent(); 176 | if (randomUA) headers.set("User-Agent", randomUA); 177 | if (CONFIG.ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION) { 178 | const randomLang = userAgentManager.getRandomAcceptLanguage(); 179 | if (randomLang) headers.set("Accept-Language", randomLang); 180 | } 181 | headers.set("Host", targetUrl.hostname); 182 | headers.set("Connection", "close"); 183 | const port = targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80); 184 | const socket = await createNewConnection(proxyConfig.host, proxyConfig.port, false); 185 | try { 186 | const writer = socket.writable.getWriter(); 187 | const reader = socket.readable.getReader(); 188 | await performSocks5Handshake(reader, writer, targetUrl.hostname, port, proxyConfig.username, proxyConfig.password); 189 | let requestStr = `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n`; 190 | headers.forEach((v, k) => requestStr += `${k}: ${v}\r\n`); 191 | requestStr += '\r\n'; 192 | await writer.write(encoder.encode(requestStr)); 193 | if (req.body) for await (const chunk of req.body) await writer.write(chunk); 194 | writer.releaseLock(); 195 | return await parseResponse(reader, socket); 196 | } catch (e) { 197 | if (!socket.closed) socket.close(); 198 | throw e; 199 | } 200 | } catch (error) { 201 | log('Socks5 代理请求失败:', error.message); 202 | throw error; 203 | } 204 | } 205 | 206 | async function createNewConnection(hostname, port, isSecure) { 207 | return connect({ hostname, port: Number(port) }, { secureTransport: isSecure ? "on" : "off", allowHalfOpen: true }); 208 | } 209 | 210 | function initializeManagers() { 211 | if (!userAgentManager) userAgentManager = new UserAgentManager(); 212 | } 213 | 214 | function concatUint8Arrays(arr1, arr2) { 215 | const result = new Uint8Array(arr1.length + arr2.length); 216 | result.set(arr1); 217 | result.set(arr2, arr1.length); 218 | return result; 219 | } 220 | 221 | function findSubarray(arr, subarr, start = 0) { 222 | for (let i = start; i <= arr.length - subarr.length; i++) { 223 | let found = true; 224 | for (let j = 0; j < subarr.length; j++) if (arr[i + j] !== subarr[j]) { found = false; break; } 225 | if (found) return i; 226 | } 227 | return -1; 228 | } 229 | 230 | const CRLF = new Uint8Array([13, 10]); 231 | const HEADER_END_MARKER = new Uint8Array([13, 10, 13, 10]); 232 | 233 | // 修复:使用 indexOf(': ') 来健壮地解析HTTP头部 234 | function parseHttpHeaders(buff) { 235 | const headerEndIndex = findSubarray(buff, HEADER_END_MARKER); 236 | if (headerEndIndex === -1) return null; 237 | const text = decoder.decode(buff.slice(0, headerEndIndex)); 238 | const lines = text.split("\r\n"); 239 | const statusMatch = lines[0].match(/HTTP\/1\.[01] (\d+) (.*)/); 240 | if (!statusMatch) throw new Error(`无效状态行: ${lines[0]}`); 241 | const headers = new Headers(); 242 | for (let i = 1; i < lines.length; i++) { 243 | const idx = lines[i].indexOf(': '); 244 | if (idx !== -1) { 245 | const key = lines[i].slice(0, idx); 246 | const value = lines[i].slice(idx + 2); 247 | headers.append(key, value); 248 | } 249 | } 250 | return { status: Number(statusMatch[1]), statusText: statusMatch[2], headers, headerEnd: headerEndIndex }; 251 | } 252 | 253 | // 增强:读取分块数据,使用动态缓冲区并处理 Trailer Headers 254 | async function* readChunks(reader, initialData) { 255 | let buffer = initialData; 256 | let offset = 0; 257 | while (true) { 258 | let lineEnd = findSubarray(buffer, CRLF, offset); 259 | while (lineEnd === -1) { 260 | const { value, done } = await reader.read(); 261 | if (done) { 262 | if (buffer.length > offset) throw new Error("分块编码:流在解析分块大小时意外结束"); 263 | return; 264 | } 265 | if (offset > 0) { 266 | buffer = buffer.slice(offset); 267 | offset = 0; 268 | } 269 | buffer = concatUint8Arrays(buffer, value); 270 | lineEnd = findSubarray(buffer, CRLF, offset); 271 | } 272 | const sizeHex = decoder.decode(buffer.slice(offset, lineEnd)).trim(); 273 | const size = parseInt(sizeHex, 16); 274 | if (isNaN(size)) throw new Error(`无效的分块大小: ${sizeHex}`); 275 | offset = lineEnd + 2; 276 | if (size === 0) { 277 | log("读取到最后一个分块 (size=0),处理 Trailer"); 278 | // 简化:循环直到找到一个空行(即连续的CRLF) 279 | while (findSubarray(buffer, CRLF, offset) !== offset) { 280 | let nextLine = findSubarray(buffer, CRLF, offset); 281 | if (nextLine !== -1) { 282 | offset = nextLine + 2; // 跳过 trailer 行 283 | continue; 284 | } 285 | const { value, done } = await reader.read(); 286 | if (done) throw new Error("分块编码:流在解析 Trailer 时意外结束"); 287 | buffer = concatUint8Arrays(buffer.slice(offset), value); 288 | offset = 0; 289 | } 290 | log("分块传输结束"); 291 | return; 292 | } 293 | while (buffer.length < offset + size + 2) { 294 | const { value, done } = await reader.read(); 295 | if (done) throw new Error("分块编码:数据块不完整"); 296 | buffer = concatUint8Arrays(buffer, value); 297 | } 298 | yield buffer.slice(offset, offset + size); 299 | offset += size + 2; 300 | } 301 | } 302 | 303 | async function parseResponse(reader, socket) { 304 | let buff = new Uint8Array(); 305 | try { 306 | while (true) { 307 | const { value, done } = await reader.read(); 308 | if (value) buff = concatUint8Arrays(buff, value); 309 | if (done && !buff.length) throw new Error("无法解析响应:流提前结束且无数据"); 310 | const parsed = parseHttpHeaders(buff); 311 | if (parsed) { 312 | const { status, statusText, headers, headerEnd } = parsed; 313 | const isChunked = headers.get("transfer-encoding")?.toLowerCase().includes("chunked"); 314 | const contentLength = parseInt(headers.get("content-length") || "0", 10); 315 | const initialBodyData = buff.slice(headerEnd + 4); 316 | return new Response(new ReadableStream({ 317 | async start(ctrl) { 318 | try { 319 | if (isChunked) { 320 | log("响应模式:分块编码 (Chunked)"); 321 | for await (const chunk of readChunks(reader, initialBodyData)) ctrl.enqueue(chunk); 322 | } else { 323 | log(`响应模式:定长 (Content-Length: ${contentLength})`); 324 | let received = initialBodyData.length; 325 | if (received > 0) ctrl.enqueue(initialBodyData); 326 | while (received < contentLength) { 327 | const { value, done } = await reader.read(); 328 | if (done) { log("警告:流在达到Content-Length之前结束"); break; } 329 | received += value.length; 330 | ctrl.enqueue(value); 331 | } 332 | } 333 | ctrl.close(); 334 | } catch (err) { 335 | log("流式响应处理错误", err); 336 | ctrl.error(err); 337 | } finally { 338 | if (socket && !socket.closed) socket.close(); 339 | } 340 | }, 341 | cancel() { 342 | log("流被客户端取消"); 343 | if (socket && !socket.closed) socket.close(); 344 | }, 345 | }), { status, statusText, headers }); 346 | } 347 | if (done) throw new Error("无法解析响应头:流已结束"); 348 | } 349 | } catch (error) { 350 | log("解析响应时发生错误", error); 351 | if (socket && !socket.closed) socket.close(); 352 | throw error; 353 | } 354 | } 355 | 356 | async function nativeFetch(req, dstUrl) { 357 | const targetUrl = new URL(dstUrl); 358 | try { 359 | const headers = new Headers(req.headers); 360 | for (const key of req.headers.keys()) if (HEADER_FILTER_RE.test(key)) headers.delete(key); 361 | const ua = userAgentManager.getCompatibleUserAgent(req.headers.get('user-agent')); 362 | if (ua) headers.set('User-Agent', ua); 363 | if (CONFIG.ENABLE_ACCEPT_LANGUAGE_RANDOMIZATION) { 364 | const lang = userAgentManager.getRandomAcceptLanguage(); 365 | if(lang) headers.set('Accept-Language', lang); 366 | } 367 | headers.set("Host", targetUrl.hostname); 368 | headers.set("Connection", "close"); 369 | const port = targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80); 370 | const socket = await createNewConnection(targetUrl.hostname, port, targetUrl.protocol === "https:"); 371 | try { 372 | const writer = socket.writable.getWriter(); 373 | let requestStr = `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n`; 374 | headers.forEach((v, k) => requestStr += `${k}: ${v}\r\n`); 375 | requestStr += '\r\n'; 376 | await writer.write(encoder.encode(requestStr)); 377 | if (req.body) for await (const chunk of req.body) await writer.write(chunk); 378 | writer.releaseLock(); 379 | return await parseResponse(socket.readable.getReader(), socket); 380 | } catch (error) { 381 | if (!socket.closed) socket.close(); 382 | throw error; 383 | } 384 | } catch (error) { 385 | log('直连失败,尝试 Socks5 Fallback:', error.message); 386 | if (CONFIG.ENABLE_SOCKS5_FALLBACK) { 387 | try { return await fetchViaSocks5(req, dstUrl); } 388 | catch (socks5Error) { log('Socks5 Fallback 也失败了:', socks5Error.message); throw socks5Error; } 389 | } 390 | throw error; 391 | } 392 | } 393 | 394 | async function smartFetch(req, dstUrl) { 395 | const hostname = new URL(dstUrl).hostname; 396 | const requestMethod = getRequestMethodForHost(hostname); 397 | log(`主机 ${hostname} 使用请求方式: ${requestMethod}`); 398 | try { 399 | return requestMethod === 'socks5' ? await fetchViaSocks5(req, dstUrl) : await nativeFetch(req, dstUrl); 400 | } catch (error) { 401 | log(`智能路由请求失败 (${requestMethod}):`, error.message); 402 | throw error; 403 | } 404 | } 405 | 406 | async function smartFetchWithRetry(req, dstUrl, maxRetries = 2) { 407 | const isIdempotent = !['POST', 'PATCH'].includes(req.method.toUpperCase()); 408 | if (!isIdempotent) { 409 | log(`请求方法为 ${req.method},非幂等,不进行重试`); 410 | return await smartFetch(req, dstUrl); 411 | } 412 | for (let i = 0; i <= maxRetries; i++) { 413 | try { 414 | const reqClone = req.clone(); 415 | const response = await smartFetch(reqClone, dstUrl); 416 | if (response.status >= 500 && i < maxRetries) { 417 | log(`收到 ${response.status} 错误,将在 ${Math.pow(2, i)} 秒后重试...`); 418 | await response.body?.cancel(); // 使用 cancel 高效关闭连接 419 | throw new Error(`Server error: ${response.status}`); 420 | } 421 | return response; 422 | } catch (error) { 423 | log(`第 ${i + 1} 次尝试失败: ${error.message}`); 424 | if (i === maxRetries) { 425 | log("已达到最大重试次数,抛出错误"); 426 | throw error; 427 | } 428 | const delay = Math.pow(2, i) * 1000; 429 | await new Promise(resolve => setTimeout(resolve, delay)); 430 | } 431 | } 432 | throw new Error("重试逻辑异常结束"); 433 | } 434 | 435 | async function handleRequest(req, env) { 436 | CONFIG = { ...DEFAULT_CONFIG, ...env }; 437 | updateConfigFromEnv(env); 438 | initializeManagers(); 439 | log = CONFIG.DEBUG_MODE ? (message, data = "") => console.log(`[${new Date().toISOString()}] ${message}`, data) : () => {}; 440 | const url = new URL(req.url); 441 | try { 442 | const pathSegments = url.pathname.split('/').filter(Boolean); 443 | if (pathSegments.length === 0) { 444 | const dstUrl = CONFIG.DEFAULT_DST_URL + url.search; 445 | return await smartFetchWithRetry(req, dstUrl); 446 | } 447 | const authToken = pathSegments[0]; 448 | if (authToken === "defaulttoken") return new Response("请修改默认AUTH_TOKEN", { status: 401 }); 449 | if (authToken !== CONFIG.AUTH_TOKEN || pathSegments.length < 2) { 450 | return new Response("Invalid path. Expected `/{authtoken}/{target_url}` or `/{preset}`.", { status: 400 }); 451 | } 452 | 453 | const presetCandidate = pathSegments[1]; 454 | let resolvedUrl; 455 | 456 | if (URL_PRESETS.has(presetCandidate)) { 457 | resolvedUrl = URL_PRESETS.get(presetCandidate); 458 | const remainingPath = pathSegments.slice(2).join('/'); 459 | if (remainingPath) { 460 | resolvedUrl += '/' + remainingPath; 461 | } 462 | } else { 463 | const authtokenPrefix = `/${authToken}/`; 464 | let targetUrl = decodeURIComponent(url.pathname.substring(url.pathname.indexOf(authtokenPrefix) + authtokenPrefix.length)); 465 | resolvedUrl = resolveUrl(targetUrl); // resolveUrl will just return targetUrl if not in presets 466 | if (!/^https?:\/\//i.test(resolvedUrl)) { 467 | return new Response("Invalid target URL. Protocol (http/https) is required.", { status: 400 }); 468 | } 469 | } 470 | 471 | const dstUrl = resolvedUrl + url.search; 472 | log("目标URL", dstUrl); 473 | return await smartFetchWithRetry(req, dstUrl); 474 | } catch (error) { 475 | log("请求处理失败", error); 476 | return new Response("Bad Gateway", { status: 502 }); 477 | } 478 | } 479 | 480 | export default { fetch: handleRequest }; 481 | export const onRequest = (ctx) => handleRequest(ctx.request, ctx.env); -------------------------------------------------------------------------------- /single.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SpectreProxy 3 | * 支持的代理策略: 4 | * - socket: 使用Cloudflare Socket API (默认) 5 | * - fetch: 使用Fetch API 6 | * - socks5: 使用SOCKS5代理 7 | * - thirdparty: 使用第三方代理服务 8 | * - cloudprovider: 使用其他云服务商函数 9 | * 环境变量配置: 10 | * - AUTH_TOKEN: 认证令牌,务必修改 11 | * - PROXY_STRATEGY: 主代理策略 (默认: "socket") 12 | * - FALLBACK_PROXY_STRATEGY: 备用代理策略 (默认: "fetch") 13 | * - DEBUG_MODE: 调试模式 (默认: false) 14 | * - SOCKS5_ADDRESS: SOCKS5代理地址 15 | * - THIRD_PARTY_PROXY_URL: 第三方代理URL 16 | * - CLOUD_PROVIDER_URL: 云服务商函数URL 17 | * - DOH_SERVER_HOSTNAME: DoH服务器主机名 (默认: "dns.google") 18 | * - DOT_SERVER_HOSTNAME: DoT服务器主机名 (默认: "dns.google") 19 | */ 20 | 21 | import { connect } from 'cloudflare:sockets'; 22 | 23 | class ConfigManager { 24 | static DEFAULT_CONFIG = { 25 | AUTH_TOKEN: "your-auth-token",//认证令牌,务必在此修改或添加环境变量 26 | DEFAULT_DST_URL: "https://httpbin.org/get", 27 | DEBUG_MODE: false, 28 | PROXY_STRATEGY: "socket", 29 | FALLBACK_PROXY_STRATEGY: "fetch", 30 | SOCKS5_ADDRESS: "", 31 | THIRD_PARTY_PROXY_URL: "", 32 | CLOUD_PROVIDER_URL: "", 33 | DOH_SERVER_HOSTNAME: "dns.google", 34 | DOH_SERVER_PORT: 443, 35 | DOH_SERVER_PATH: "/dns-query", 36 | DOT_SERVER_HOSTNAME: "dns.google", 37 | DOT_SERVER_PORT: 853, 38 | }; 39 | 40 | static updateConfigFromEnv(env) { 41 | if (!env) return { ...this.DEFAULT_CONFIG }; 42 | 43 | const config = { ...this.DEFAULT_CONFIG }; 44 | 45 | for (const key of Object.keys(config)) { 46 | if (key in env) { 47 | if (typeof config[key] === 'boolean') { 48 | config[key] = env[key] === 'true'; 49 | } else { 50 | config[key] = env[key]; 51 | } 52 | } 53 | } 54 | 55 | return config; 56 | } 57 | 58 | static getConfigValue(config, key, defaultValue = null) { 59 | return config[key] !== undefined ? config[key] : defaultValue; 60 | } 61 | } 62 | 63 | class BaseProxy { 64 | constructor(config) { 65 | this.config = config; 66 | this.encoder = new TextEncoder(); 67 | this.decoder = new TextDecoder(); 68 | 69 | this.log = config.DEBUG_MODE 70 | ? (message, data = "") => console.log(`[DEBUG] ${message}`, data) 71 | : () => {}; 72 | } 73 | 74 | async connect(req, dstUrl) { 75 | throw new Error("connect method must be implemented by subclass"); 76 | } 77 | 78 | async connectWebSocket(req, dstUrl) { 79 | throw new Error("connectWebSocket method must be implemented by subclass"); 80 | } 81 | 82 | async connectHttp(req, dstUrl) { 83 | throw new Error("connectHttp method must be implemented by subclass"); 84 | } 85 | 86 | async handleDnsQuery(req) { 87 | return new Response("DNS query handling not implemented for this proxy type", { status: 501 }); 88 | } 89 | 90 | handleError(error, context, status = 500) { 91 | this.log(`${context} failed`, error.message); 92 | return new Response(`Error ${context.toLowerCase()}: ${error.message}`, { status }); 93 | } 94 | 95 | isCloudflareNetworkError(error) { 96 | return false; 97 | } 98 | 99 | async connectHttpViaProxy(req, dstUrl, proxyUrl, proxyType) { 100 | const targetUrl = new URL(dstUrl); 101 | const proxyUrlObj = new URL(proxyUrl); 102 | proxyUrlObj.searchParams.set('target', dstUrl); 103 | 104 | const cleanedHeaders = this.filterHeaders(req.headers); 105 | 106 | cleanedHeaders.set("Host", proxyUrlObj.hostname); 107 | 108 | try { 109 | const fetchRequest = new Request(proxyUrlObj.toString(), { 110 | method: req.method, 111 | headers: cleanedHeaders, 112 | body: req.body, 113 | }); 114 | 115 | this.log(`Using ${proxyType} proxy to connect to`, dstUrl); 116 | return await fetch(fetchRequest); 117 | } catch (error) { 118 | return this.handleError(error, `${proxyType} proxy connection`); 119 | } 120 | } 121 | 122 | filterHeaders(headers) { 123 | const HEADER_FILTER_RE = /^(host|accept-encoding|cf-|cdn-|referer|referrer)/i; 124 | const cleanedHeaders = new Headers(); 125 | 126 | for (const [k, v] of headers) { 127 | if (!HEADER_FILTER_RE.test(k)) { 128 | cleanedHeaders.set(k, v); 129 | } 130 | } 131 | 132 | return cleanedHeaders; 133 | } 134 | 135 | generateWebSocketKey() { 136 | const bytes = new Uint8Array(16); 137 | crypto.getRandomValues(bytes); 138 | return btoa(String.fromCharCode(...bytes)); 139 | } 140 | 141 | relayWebSocketFrames(ws, socket, writer, reader) { 142 | ws.addEventListener("message", async (event) => { 143 | let payload; 144 | if (typeof event.data === "string") { 145 | payload = this.encoder.encode(event.data); 146 | } else if (event.data instanceof ArrayBuffer) { 147 | payload = new Uint8Array(event.data); 148 | } else { 149 | payload = event.data; 150 | } 151 | const frame = this.packTextFrame(payload); 152 | try { 153 | await writer.write(frame); 154 | } catch (e) { 155 | this.log("Remote write error", e); 156 | } 157 | }); 158 | 159 | (async () => { 160 | const frameReader = new this.SocketFramesReader(reader, this); 161 | try { 162 | while (true) { 163 | const frame = await frameReader.nextFrame(); 164 | if (!frame) break; 165 | switch (frame.opcode) { 166 | case 1: 167 | case 2: 168 | ws.send(frame.payload); 169 | break; 170 | case 8: 171 | this.log("Received Close frame, closing WebSocket"); 172 | ws.close(1000); 173 | return; 174 | default: 175 | this.log(`Received unknown frame type, Opcode: ${frame.opcode}`); 176 | } 177 | } 178 | } catch (e) { 179 | this.log("Error reading remote frame", e); 180 | } finally { 181 | ws.close(); 182 | writer.releaseLock(); 183 | socket.close(); 184 | } 185 | })(); 186 | 187 | ws.addEventListener("close", () => socket.close()); 188 | } 189 | 190 | packTextFrame(payload) { 191 | const FIN_AND_OP = 0x81; 192 | const maskBit = 0x80; 193 | const len = payload.length; 194 | let header; 195 | if (len < 126) { 196 | header = new Uint8Array(2); 197 | header[0] = FIN_AND_OP; 198 | header[1] = maskBit | len; 199 | } else if (len < 65536) { 200 | header = new Uint8Array(4); 201 | header[0] = FIN_AND_OP; 202 | header[1] = maskBit | 126; 203 | header[2] = (len >> 8) & 0xff; 204 | header[3] = len & 0xff; 205 | } else { 206 | throw new Error("Payload too large"); 207 | } 208 | const mask = new Uint8Array(4); 209 | crypto.getRandomValues(mask); 210 | const maskedPayload = new Uint8Array(len); 211 | for (let i = 0; i < len; i++) { 212 | maskedPayload[i] = payload[i] ^ mask[i % 4]; 213 | } 214 | return this.concatUint8Arrays(header, mask, maskedPayload); 215 | } 216 | 217 | SocketFramesReader = class { 218 | constructor(reader, parent) { 219 | this.reader = reader; 220 | this.parent = parent; 221 | this.buffer = new Uint8Array(); 222 | this.fragmentedPayload = null; 223 | this.fragmentedOpcode = null; 224 | } 225 | 226 | async ensureBuffer(length) { 227 | while (this.buffer.length < length) { 228 | const { value, done } = await this.reader.read(); 229 | if (done) return false; 230 | this.buffer = this.parent.concatUint8Arrays(this.buffer, value); 231 | } 232 | return true; 233 | } 234 | 235 | async nextFrame() { 236 | while (true) { 237 | if (!(await this.ensureBuffer(2))) return null; 238 | const first = this.buffer[0], 239 | second = this.buffer[1], 240 | fin = (first >> 7) & 1, 241 | opcode = first & 0x0f, 242 | isMasked = (second >> 7) & 1; 243 | let payloadLen = second & 0x7f, 244 | offset = 2; 245 | if (payloadLen === 126) { 246 | if (!(await this.ensureBuffer(offset + 2))) return null; 247 | payloadLen = (this.buffer[offset] << 8) | this.buffer[offset + 1]; 248 | offset += 2; 249 | } else if (payloadLen === 127) { 250 | throw new Error("127 length mode is not supported"); 251 | } 252 | let mask; 253 | if (isMasked) { 254 | if (!(await this.ensureBuffer(offset + 4))) return null; 255 | mask = this.buffer.slice(offset, offset + 4); 256 | offset += 4; 257 | } 258 | if (!(await this.ensureBuffer(offset + payloadLen))) return null; 259 | let payload = this.buffer.slice(offset, offset + payloadLen); 260 | if (isMasked && mask) { 261 | for (let i = 0; i < payload.length; i++) { 262 | payload[i] ^= mask[i % 4]; 263 | } 264 | } 265 | this.buffer = this.buffer.slice(offset + payloadLen); 266 | if (opcode === 0) { 267 | if (this.fragmentedPayload === null) 268 | throw new Error("Received continuation frame without initiation"); 269 | this.fragmentedPayload = this.parent.concatUint8Arrays(this.fragmentedPayload, payload); 270 | if (fin) { 271 | const completePayload = this.fragmentedPayload; 272 | const completeOpcode = this.fragmentedOpcode; 273 | this.fragmentedPayload = this.fragmentedOpcode = null; 274 | return { fin: true, opcode: completeOpcode, payload: completePayload }; 275 | } 276 | } else { 277 | if (!fin) { 278 | this.fragmentedPayload = payload; 279 | this.fragmentedOpcode = opcode; 280 | continue; 281 | } else { 282 | if (this.fragmentedPayload) { 283 | this.fragmentedPayload = this.fragmentedOpcode = null; 284 | } 285 | return { fin, opcode, payload }; 286 | } 287 | } 288 | } 289 | } 290 | }; 291 | 292 | concatUint8Arrays(...arrays) { 293 | const total = arrays.reduce((sum, arr) => sum + arr.length, 0); 294 | const result = new Uint8Array(total); 295 | let offset = 0; 296 | for (const arr of arrays) { 297 | result.set(arr, offset); 298 | offset += arr.length; 299 | } 300 | return result; 301 | } 302 | 303 | parseHttpHeaders(buff) { 304 | const text = this.decoder.decode(buff); 305 | const headerEnd = text.indexOf("\r\n\r\n"); 306 | if (headerEnd === -1) return null; 307 | const headerSection = text.slice(0, headerEnd).split("\r\n"); 308 | const statusLine = headerSection[0]; 309 | const statusMatch = statusLine.match(/HTTP\/1\.[01] (\d+) (.*)/); 310 | if (!statusMatch) throw new Error(`Invalid status line: ${statusLine}`); 311 | const headers = new Headers(); 312 | for (let i = 1; i < headerSection.length; i++) { 313 | const line = headerSection[i]; 314 | const idx = line.indexOf(": "); 315 | if (idx !== -1) { 316 | headers.append(line.slice(0, idx), line.slice(idx + 2)); 317 | } 318 | } 319 | return { status: Number(statusMatch[1]), statusText: statusMatch[2], headers, headerEnd }; 320 | } 321 | 322 | async readUntilDoubleCRLF(reader) { 323 | let respText = ""; 324 | while (true) { 325 | const { value, done } = await reader.read(); 326 | if (value) { 327 | respText += this.decoder.decode(value, { stream: true }); 328 | if (respText.includes("\r\n\r\n")) break; 329 | } 330 | if (done) break; 331 | } 332 | return respText; 333 | } 334 | 335 | async parseResponse(reader) { 336 | let buff = new Uint8Array(); 337 | while (true) { 338 | const { value, done } = await reader.read(); 339 | if (value) { 340 | buff = this.concatUint8Arrays(buff, value); 341 | const parsed = this.parseHttpHeaders(buff); 342 | if (parsed) { 343 | const { status, statusText, headers, headerEnd } = parsed; 344 | const isChunked = headers.get("transfer-encoding")?.includes("chunked"); 345 | const contentLength = parseInt(headers.get("content-length") || "0", 10); 346 | const data = buff.slice(headerEnd + 4); 347 | const self = this; 348 | return new Response( 349 | new ReadableStream({ 350 | start: async (ctrl) => { 351 | try { 352 | if (isChunked) { 353 | console.log("Using chunked transfer mode"); 354 | for await (const chunk of self.readChunks(reader, data)) { 355 | ctrl.enqueue(chunk); 356 | } 357 | } else { 358 | console.log("Using fixed-length transfer mode, contentLength: " + contentLength); 359 | let received = data.length; 360 | if (data.length) ctrl.enqueue(data); 361 | while (received < contentLength) { 362 | const { value, done } = await reader.read(); 363 | if (done) break; 364 | received += value.length; 365 | ctrl.enqueue(value); 366 | } 367 | } 368 | ctrl.close(); 369 | } catch (err) { 370 | console.log("Error parsing response", err); 371 | ctrl.error(err); 372 | } 373 | }, 374 | }), 375 | { status, statusText, headers } 376 | ); 377 | } 378 | } 379 | if (done) break; 380 | } 381 | throw new Error("Unable to parse response headers"); 382 | } 383 | 384 | async *readChunks(reader, buff = new Uint8Array()) { 385 | while (true) { 386 | let pos = -1; 387 | for (let i = 0; i < buff.length - 1; i++) { 388 | if (buff[i] === 13 && buff[i + 1] === 10) { 389 | pos = i; 390 | break; 391 | } 392 | } 393 | if (pos === -1) { 394 | const { value, done } = await reader.read(); 395 | if (done) break; 396 | buff = this.concatUint8Arrays(buff, value); 397 | continue; 398 | } 399 | const sizeStr = this.decoder.decode(buff.slice(0, pos)); 400 | const size = parseInt(sizeStr, 16); 401 | this.log("Read chunk size", size); 402 | if (!size) break; 403 | buff = buff.slice(pos + 2); 404 | while (buff.length < size + 2) { 405 | const { value, done } = await reader.read(); 406 | if (done) throw new Error("Unexpected EOF in chunked encoding"); 407 | buff = this.concatUint8Arrays(buff, value); 408 | } 409 | yield buff.slice(0, size); 410 | buff = buff.slice(size + 2); 411 | } 412 | } 413 | } 414 | 415 | class SocketProxy extends BaseProxy { 416 | constructor(config) { 417 | super(config); 418 | } 419 | 420 | async connect(req, dstUrl) { 421 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 422 | const isWebSocket = upgradeHeader === "websocket"; 423 | 424 | if (isWebSocket) { 425 | return await this.connectWebSocket(req, dstUrl); 426 | } else { 427 | return await this.connectHttp(req, dstUrl); 428 | } 429 | } 430 | 431 | async connectWebSocket(req, dstUrl) { 432 | const targetUrl = new URL(dstUrl); 433 | 434 | if (!/^wss?:\/\//i.test(dstUrl)) { 435 | return new Response("Target does not support WebSocket", { status: 400 }); 436 | } 437 | 438 | const isSecure = targetUrl.protocol === "wss:"; 439 | const port = targetUrl.port || (isSecure ? 443 : 80); 440 | 441 | const socket = await connect( 442 | { hostname: targetUrl.hostname, port: Number(port) }, 443 | { secureTransport: isSecure ? "on" : "off", allowHalfOpen: false } 444 | ); 445 | 446 | const key = this.generateWebSocketKey(); 447 | 448 | const cleanedHeaders = this.filterHeaders(req.headers); 449 | 450 | cleanedHeaders.set('Host', targetUrl.hostname); 451 | cleanedHeaders.set('Connection', 'Upgrade'); 452 | cleanedHeaders.set('Upgrade', 'websocket'); 453 | cleanedHeaders.set('Sec-WebSocket-Version', '13'); 454 | cleanedHeaders.set('Sec-WebSocket-Key', key); 455 | 456 | const handshakeReq = 457 | `GET ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 458 | Array.from(cleanedHeaders.entries()) 459 | .map(([k, v]) => `${k}: ${v}`) 460 | .join('\r\n') + 461 | '\r\n\r\n'; 462 | 463 | this.log("Sending WebSocket handshake request", handshakeReq); 464 | const writer = socket.writable.getWriter(); 465 | await writer.write(this.encoder.encode(handshakeReq)); 466 | 467 | const reader = socket.readable.getReader(); 468 | const handshakeResp = await this.readUntilDoubleCRLF(reader); 469 | this.log("Received handshake response", handshakeResp); 470 | 471 | if ( 472 | !handshakeResp.includes("101") || 473 | !handshakeResp.includes("Switching Protocols") 474 | ) { 475 | throw new Error("WebSocket handshake failed: " + handshakeResp); 476 | } 477 | 478 | const webSocketPair = new WebSocketPair(); 479 | const client = webSocketPair[0]; 480 | const server = webSocketPair[1]; 481 | client.accept(); 482 | 483 | this.relayWebSocketFrames(client, socket, writer, reader); 484 | return new Response(null, { status: 101, webSocket: server }); 485 | } 486 | 487 | async connectHttp(req, dstUrl) { 488 | const reqForFallback = req.clone(); 489 | const targetUrl = new URL(dstUrl); 490 | 491 | const cleanedHeaders = this.filterHeaders(req.headers); 492 | 493 | cleanedHeaders.set("Host", targetUrl.hostname); 494 | cleanedHeaders.set("accept-encoding", "identity"); 495 | 496 | try { 497 | const port = targetUrl.protocol === "https:" ? 443 : 80; 498 | const socket = await connect( 499 | { hostname: targetUrl.hostname, port: Number(port) }, 500 | { secureTransport: targetUrl.protocol === "https:" ? "on" : "off", allowHalfOpen: false } 501 | ); 502 | const writer = socket.writable.getWriter(); 503 | 504 | const requestLine = 505 | `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 506 | Array.from(cleanedHeaders.entries()) 507 | .map(([k, v]) => `${k}: ${v}`) 508 | .join("\r\n") + 509 | "\r\n\r\n"; 510 | 511 | this.log("Sending request", requestLine); 512 | await writer.write(this.encoder.encode(requestLine)); 513 | 514 | if (req.body) { 515 | this.log("Forwarding request body"); 516 | const reader = req.body.getReader(); 517 | while (true) { 518 | const { done, value } = await reader.read(); 519 | if (done) break; 520 | await writer.write(value); 521 | } 522 | } 523 | 524 | return await this.parseResponse(socket.readable.getReader()); 525 | } catch (error) { 526 | if (this.isCloudflareNetworkError(error)) { 527 | this.log("Cloudflare network restriction detected, switching to fallback proxy"); 528 | this.log("Original error:", error.message); 529 | 530 | const fallbackStrategy = this.config.FALLBACK_PROXY_STRATEGY || "fetch"; 531 | this.log("Using fallback strategy:", fallbackStrategy); 532 | 533 | const fallbackConfig = { ...this.config, PROXY_STRATEGY: fallbackStrategy }; 534 | let fallbackProxy; 535 | 536 | switch (fallbackStrategy.toLowerCase()) { 537 | case 'fetch': 538 | fallbackProxy = new FetchProxy(fallbackConfig); 539 | break; 540 | case 'socks5': 541 | fallbackProxy = new Socks5Proxy(fallbackConfig); 542 | break; 543 | case 'thirdparty': 544 | fallbackProxy = new ThirdPartyProxy(fallbackConfig); 545 | break; 546 | case 'cloudprovider': 547 | fallbackProxy = new CloudProviderProxy(fallbackConfig); 548 | break; 549 | default: 550 | fallbackProxy = new FetchProxy(fallbackConfig); 551 | } 552 | 553 | this.log("Attempting fallback connection with", fallbackStrategy); 554 | 555 | return await fallbackProxy.connectHttp(reqForFallback, dstUrl); 556 | } 557 | 558 | return this.handleError(error, "Socket connection"); 559 | } 560 | } 561 | 562 | isCloudflareNetworkError(error) { 563 | return error.message && ( 564 | error.message.includes("A network issue was detected") || 565 | error.message.includes("Network connection failure") || 566 | error.message.includes("connection failed") || 567 | error.message.includes("timed out") || 568 | error.message.includes("Stream was cancelled") || 569 | error.message.includes("proxy request failed") || 570 | error.message.includes("cannot connect to the specified address") || 571 | error.message.includes("TCP Loop detected") || 572 | error.message.includes("Connections to port 25 are prohibited") 573 | ); 574 | } 575 | 576 | async handleDnsQuery(req) { 577 | return new Response("Socket proxy does not support DNS query handling. Please use DoH or DoT proxy.", { status: 400 }); 578 | } 579 | } 580 | 581 | class FetchProxy extends BaseProxy { 582 | constructor(config) { 583 | super(config); 584 | this.UPSTREAM_DNS_SERVER = { 585 | hostname: config.DOH_SERVER_HOSTNAME || 'dns.google', 586 | path: config.DOH_SERVER_PATH || '/dns-query', 587 | }; 588 | } 589 | 590 | async connect(req, dstUrl) { 591 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 592 | const isWebSocket = upgradeHeader === "websocket"; 593 | 594 | if (isWebSocket) { 595 | return new Response("Fetch proxy does not support WebSocket", { status: 400 }); 596 | } else { 597 | return await this.connectHttp(req, dstUrl); 598 | } 599 | } 600 | 601 | async connectHttp(req, dstUrl) { 602 | const targetUrl = new URL(dstUrl); 603 | 604 | const cleanedHeaders = this.filterHeaders(req.headers); 605 | 606 | cleanedHeaders.set("Host", targetUrl.hostname); 607 | 608 | try { 609 | const fetchRequest = new Request(dstUrl, { 610 | method: req.method, 611 | headers: cleanedHeaders, 612 | body: req.body, 613 | }); 614 | 615 | this.log("Using fetch to connect to", dstUrl); 616 | return await fetch(fetchRequest); 617 | } catch (error) { 618 | return this.handleError(error, "Fetch connection"); 619 | } 620 | } 621 | 622 | async connectWebSocket(req, dstUrl) { 623 | return new Response("Fetch proxy does not support WebSocket", { status: 400 }); 624 | } 625 | 626 | async handleDnsQuery(req) { 627 | try { 628 | const upstreamDnsUrl = `https://${this.UPSTREAM_DNS_SERVER.hostname}${this.UPSTREAM_DNS_SERVER.path}`; 629 | 630 | const cleanedHeaders = this.filterHeaders(req.headers); 631 | 632 | cleanedHeaders.set("Host", this.UPSTREAM_DNS_SERVER.hostname); 633 | cleanedHeaders.set("Content-Type", "application/dns-message"); 634 | cleanedHeaders.set("Accept", "application/dns-message"); 635 | 636 | const fetchRequest = new Request(upstreamDnsUrl, { 637 | method: req.method, 638 | headers: cleanedHeaders, 639 | body: req.body, 640 | }); 641 | 642 | this.log("Using fetch to handle DNS query"); 643 | return await fetch(fetchRequest); 644 | } catch (error) { 645 | return this.handleError(error, "Fetch DNS query handling", 502); 646 | } 647 | } 648 | } 649 | 650 | class Socks5Proxy extends BaseProxy { 651 | constructor(config) { 652 | super(config); 653 | this.parsedSocks5Address = this.parseSocks5Address(config.SOCKS5_ADDRESS); 654 | } 655 | 656 | async connect(req, dstUrl) { 657 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 658 | const isWebSocket = upgradeHeader === "websocket"; 659 | 660 | if (isWebSocket) { 661 | return await this.connectWebSocket(req, dstUrl); 662 | } else { 663 | return await this.connectHttp(req, dstUrl); 664 | } 665 | } 666 | 667 | async connectWebSocket(req, dstUrl) { 668 | const targetUrl = new URL(dstUrl); 669 | 670 | if (!/^wss?:\/\//i.test(dstUrl)) { 671 | return new Response("Target does not support WebSocket", { status: 400 }); 672 | } 673 | 674 | const socket = await this.socks5Connect( 675 | 2, 676 | targetUrl.hostname, 677 | Number(targetUrl.port) || (targetUrl.protocol === "wss:" ? 443 : 80) 678 | ); 679 | 680 | const key = this.generateWebSocketKey(); 681 | 682 | const cleanedHeaders = this.filterHeaders(req.headers); 683 | 684 | cleanedHeaders.set('Host', targetUrl.hostname); 685 | cleanedHeaders.set('Connection', 'Upgrade'); 686 | cleanedHeaders.set('Upgrade', 'websocket'); 687 | cleanedHeaders.set('Sec-WebSocket-Version', '13'); 688 | cleanedHeaders.set('Sec-WebSocket-Key', key); 689 | 690 | const handshakeReq = 691 | `GET ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 692 | Array.from(cleanedHeaders.entries()) 693 | .map(([k, v]) => `${k}: ${v}`) 694 | .join('\r\n') + 695 | '\r\n\r\n'; 696 | 697 | this.log("Sending WebSocket handshake request", handshakeReq); 698 | const writer = socket.writable.getWriter(); 699 | await writer.write(this.encoder.encode(handshakeReq)); 700 | 701 | const reader = socket.readable.getReader(); 702 | const handshakeResp = await this.readUntilDoubleCRLF(reader); 703 | this.log("Received handshake response", handshakeResp); 704 | 705 | if ( 706 | !handshakeResp.includes("101") || 707 | !handshakeResp.includes("Switching Protocols") 708 | ) { 709 | throw new Error("WebSocket handshake failed: " + handshakeResp); 710 | } 711 | 712 | const webSocketPair = new WebSocketPair(); 713 | const client = webSocketPair[0]; 714 | const server = webSocketPair[1]; 715 | client.accept(); 716 | 717 | this.relayWebSocketFrames(client, socket, writer, reader); 718 | return new Response(null, { status: 101, webSocket: server }); 719 | } 720 | 721 | async connectHttp(req, dstUrl) { 722 | const targetUrl = new URL(dstUrl); 723 | 724 | const cleanedHeaders = this.filterHeaders(req.headers); 725 | 726 | cleanedHeaders.set("Host", targetUrl.hostname); 727 | cleanedHeaders.set("accept-encoding", "identity"); 728 | 729 | try { 730 | const socket = await this.socks5Connect( 731 | 2, 732 | targetUrl.hostname, 733 | Number(targetUrl.port) || (targetUrl.protocol === "https:" ? 443 : 80) 734 | ); 735 | 736 | const writer = socket.writable.getWriter(); 737 | 738 | const requestLine = 739 | `${req.method} ${targetUrl.pathname}${targetUrl.search} HTTP/1.1\r\n` + 740 | Array.from(cleanedHeaders.entries()) 741 | .map(([k, v]) => `${k}: ${v}`) 742 | .join("\r\n") + 743 | "\r\n\r\n"; 744 | 745 | this.log("Sending request", requestLine); 746 | await writer.write(this.encoder.encode(requestLine)); 747 | 748 | if (req.body) { 749 | this.log("Forwarding request body"); 750 | const reader = req.body.getReader(); 751 | while (true) { 752 | const { done, value } = await reader.read(); 753 | if (done) break; 754 | await writer.write(value); 755 | } 756 | } 757 | 758 | return await this.parseResponse(socket.readable.getReader()); 759 | } catch (error) { 760 | return this.handleError(error, "SOCKS5 connection"); 761 | } 762 | } 763 | 764 | async socks5Connect(addressType, addressRemote, portRemote) { 765 | const { username, password, hostname, port } = this.parsedSocks5Address; 766 | const socket = connect({ 767 | hostname, 768 | port, 769 | }); 770 | 771 | const socksGreeting = new Uint8Array([5, 2, 0, 2]); 772 | 773 | const writer = socket.writable.getWriter(); 774 | 775 | await writer.write(socksGreeting); 776 | this.log('sent socks greeting'); 777 | 778 | const reader = socket.readable.getReader(); 779 | const encoder = new TextEncoder(); 780 | let res = (await reader.read()).value; 781 | if (res[0] !== 0x05) { 782 | this.log(`socks server version error: ${res[0]} expected: 5`); 783 | throw new Error(`socks server version error: ${res[0]} expected: 5`); 784 | } 785 | if (res[1] === 0xff) { 786 | this.log("no acceptable methods"); 787 | throw new Error("no acceptable methods"); 788 | } 789 | 790 | if (res[1] === 0x02) { 791 | this.log("socks server needs auth"); 792 | if (!username || !password) { 793 | this.log("please provide username/password"); 794 | throw new Error("please provide username/password"); 795 | } 796 | const authRequest = new Uint8Array([ 797 | 1, 798 | username.length, 799 | ...encoder.encode(username), 800 | password.length, 801 | ...encoder.encode(password) 802 | ]); 803 | await writer.write(authRequest); 804 | res = (await reader.read()).value; 805 | if (res[0] !== 0x01 || res[1] !== 0x00) { 806 | this.log("fail to auth socks server"); 807 | throw new Error("fail to auth socks server"); 808 | } 809 | } 810 | 811 | let DSTADDR; 812 | switch (addressType) { 813 | case 1: 814 | DSTADDR = new Uint8Array( 815 | [1, ...addressRemote.split('.').map(Number)] 816 | ); 817 | break; 818 | case 2: 819 | DSTADDR = new Uint8Array( 820 | [3, addressRemote.length, ...encoder.encode(addressRemote)] 821 | ); 822 | break; 823 | case 3: 824 | DSTADDR = new Uint8Array( 825 | [4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])] 826 | ); 827 | break; 828 | default: 829 | this.log(`invalid addressType is ${addressType}`); 830 | throw new Error(`invalid addressType is ${addressType}`); 831 | } 832 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); 833 | await writer.write(socksRequest); 834 | this.log('sent socks request'); 835 | 836 | res = (await reader.read()).value; 837 | if (res[1] === 0x00) { 838 | this.log("socks connection opened"); 839 | } else { 840 | this.log("fail to open socks connection"); 841 | throw new Error("fail to open socks connection"); 842 | } 843 | writer.releaseLock(); 844 | reader.releaseLock(); 845 | return socket; 846 | } 847 | 848 | parseSocks5Address(address) { 849 | let [latter, former] = address.split("@").reverse(); 850 | let username, password, hostname, port; 851 | if (former) { 852 | const formers = former.split(":"); 853 | if (formers.length !== 2) { 854 | throw new Error('Invalid SOCKS address format'); 855 | } 856 | [username, password] = formers; 857 | } 858 | const latters = latter.split(":"); 859 | port = Number(latters.pop()); 860 | if (isNaN(port)) { 861 | throw new Error('Invalid SOCKS address format'); 862 | } 863 | hostname = latters.join(":"); 864 | const regex = /^\[.*\]$/; 865 | if (hostname.includes(":") && !regex.test(hostname)) { 866 | throw new Error('Invalid SOCKS address format'); 867 | } 868 | return { 869 | username, 870 | password, 871 | hostname, 872 | port, 873 | } 874 | } 875 | } 876 | 877 | class ThirdPartyProxy extends BaseProxy { 878 | constructor(config) { 879 | super(config); 880 | } 881 | 882 | async connect(req, dstUrl) { 883 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 884 | const isWebSocket = upgradeHeader === "websocket"; 885 | 886 | if (isWebSocket) { 887 | return new Response("Third party proxy may not support WebSocket", { status: 400 }); 888 | } else { 889 | return await this.connectHttp(req, dstUrl); 890 | } 891 | } 892 | 893 | async connectHttp(req, dstUrl) { 894 | const thirdPartyProxyUrl = this.config.THIRD_PARTY_PROXY_URL; 895 | if (!thirdPartyProxyUrl) { 896 | return this.handleError(new Error("Third party proxy URL is not configured"), "Third party proxy connection", 500); 897 | } 898 | 899 | const proxyUrlObj = new URL(thirdPartyProxyUrl); 900 | proxyUrlObj.searchParams.set('target', dstUrl); 901 | 902 | const proxyRequest = new Request(proxyUrlObj.toString(), { 903 | method: req.method, 904 | headers: req.headers, 905 | body: req.body, 906 | redirect: 'manual', 907 | }); 908 | 909 | try { 910 | this.log(`Using third party proxy via fetch to connect to`, dstUrl); 911 | return await fetch(proxyRequest); 912 | } catch (error) { 913 | return this.handleError(error, "Third party proxy connection"); 914 | } 915 | } 916 | 917 | async connectWebSocket(req, dstUrl) { 918 | return new Response("Third party proxy may not support WebSocket", { status: 400 }); 919 | } 920 | } 921 | 922 | class CloudProviderProxy extends BaseProxy { 923 | constructor(config) { 924 | super(config); 925 | } 926 | 927 | async connect(req, dstUrl) { 928 | const upgradeHeader = req.headers.get("Upgrade")?.toLowerCase(); 929 | const isWebSocket = upgradeHeader === "websocket"; 930 | 931 | if (isWebSocket) { 932 | return new Response("Cloud provider proxy may not support WebSocket", { status: 400 }); 933 | } else { 934 | return await this.connectHttp(req, dstUrl); 935 | } 936 | } 937 | 938 | async connectHttp(req, dstUrl) { 939 | const cloudProviderUrl = this.config.CLOUD_PROVIDER_URL; 940 | if (!cloudProviderUrl) { 941 | return this.handleError(new Error("Cloud provider URL is not configured"), "Cloud provider proxy connection", 500); 942 | } 943 | 944 | const proxyUrlObj = new URL(cloudProviderUrl); 945 | proxyUrlObj.searchParams.set('target', dstUrl); 946 | 947 | const proxyRequest = new Request(proxyUrlObj.toString(), { 948 | method: req.method, 949 | headers: req.headers, 950 | body: req.body, 951 | redirect: 'manual', 952 | }); 953 | 954 | try { 955 | this.log(`Using cloud provider proxy via fetch to connect to`, dstUrl); 956 | return await fetch(proxyRequest); 957 | } catch (error) { 958 | return this.handleError(error, "Cloud provider proxy connection"); 959 | } 960 | } 961 | 962 | async connectWebSocket(req, dstUrl) { 963 | return new Response("Cloud provider proxy may not support WebSocket", { status: 400 }); 964 | } 965 | } 966 | 967 | class DoHProxy extends BaseProxy { 968 | constructor(config) { 969 | super(config); 970 | this.UPSTREAM_DOH_SERVER = { 971 | hostname: config.DOH_SERVER_HOSTNAME || 'dns.google', 972 | port: config.DOH_SERVER_PORT || 443, 973 | path: config.DOH_SERVER_PATH || '/dns-query', 974 | }; 975 | } 976 | 977 | async handleDnsQuery(req) { 978 | if (req.method !== 'POST' || req.headers.get('content-type') !== 'application/dns-message') { 979 | return new Response('This is a DNS proxy. Please use a DoH client.', { status: 400 }); 980 | } 981 | 982 | let clientDnsQuery; 983 | try { 984 | clientDnsQuery = await req.arrayBuffer(); 985 | 986 | const cleanedHeaders = this.filterHeaders(req.headers); 987 | 988 | cleanedHeaders.set('Host', this.UPSTREAM_DOH_SERVER.hostname); 989 | cleanedHeaders.set('Content-Type', 'application/dns-message'); 990 | cleanedHeaders.set('Content-Length', clientDnsQuery.byteLength.toString()); 991 | cleanedHeaders.set('Accept', 'application/dns-message'); 992 | cleanedHeaders.set('Connection', 'close'); 993 | 994 | const socket = connect(this.UPSTREAM_DOH_SERVER, { secureTransport: 'on', allowHalfOpen: false }); 995 | const writer = socket.writable.getWriter(); 996 | 997 | const httpHeaders = 998 | `POST ${this.UPSTREAM_DOH_SERVER.path} HTTP/1.1\r\n` + 999 | Array.from(cleanedHeaders.entries()) 1000 | .map(([k, v]) => `${k}: ${v}`) 1001 | .join('\r\n') + 1002 | '\r\n\r\n'; 1003 | 1004 | const requestHeaderBytes = this.encoder.encode(httpHeaders); 1005 | const requestBodyBytes = new Uint8Array(clientDnsQuery); 1006 | 1007 | const fullRequest = new Uint8Array(requestHeaderBytes.length + requestBodyBytes.length); 1008 | fullRequest.set(requestHeaderBytes, 0); 1009 | fullRequest.set(requestBodyBytes, requestHeaderBytes.length); 1010 | 1011 | await writer.write(fullRequest); 1012 | writer.releaseLock(); 1013 | 1014 | const reader = socket.readable.getReader(); 1015 | let responseBytes = new Uint8Array(); 1016 | 1017 | while (true) { 1018 | const { done, value } = await reader.read(); 1019 | if (done) break; 1020 | 1021 | const newBuffer = new Uint8Array(responseBytes.length + value.length); 1022 | newBuffer.set(responseBytes, 0); 1023 | newBuffer.set(value, responseBytes.length); 1024 | responseBytes = newBuffer; 1025 | } 1026 | 1027 | reader.releaseLock(); 1028 | await socket.close(); 1029 | 1030 | const separator = new Uint8Array([13, 10, 13, 10]); 1031 | let separatorIndex = -1; 1032 | for (let i = 0; i < responseBytes.length - 3; i++) { 1033 | if (responseBytes[i] === separator[0] && responseBytes[i + 1] === separator[1] && 1034 | responseBytes[i + 2] === separator[2] && responseBytes[i + 3] === separator[3]) { 1035 | separatorIndex = i; 1036 | break; 1037 | } 1038 | } 1039 | 1040 | if (separatorIndex === -1) { 1041 | throw new Error("Could not find HTTP header/body separator in response."); 1042 | } 1043 | 1044 | const dnsResponseBody = responseBytes.slice(separatorIndex + 4); 1045 | 1046 | return new Response(dnsResponseBody, { 1047 | headers: { 'content-type': 'application/dns-message' }, 1048 | }); 1049 | } catch (error) { 1050 | try { 1051 | const fallbackProxy = new FetchProxy(this.config); 1052 | const fallbackRequest = new Request(req.url, { 1053 | method: req.method, 1054 | headers: req.headers, 1055 | body: clientDnsQuery 1056 | }); 1057 | return await fallbackProxy.handleDnsQuery(fallbackRequest); 1058 | } catch (fallbackError) { 1059 | return this.handleError(fallbackError, "DoH proxying with connect", 502); 1060 | } 1061 | } 1062 | } 1063 | 1064 | async connect(req, dstUrl) { 1065 | return await this.handleDnsQuery(req); 1066 | } 1067 | 1068 | async connectHttp(req, dstUrl) { 1069 | return await this.handleDnsQuery(req); 1070 | } 1071 | 1072 | async connectWebSocket(req, dstUrl) { 1073 | return new Response("DoH proxy does not support WebSocket", { status: 400 }); 1074 | } 1075 | } 1076 | 1077 | class DoTProxy extends BaseProxy { 1078 | constructor(config) { 1079 | super(config); 1080 | this.UPSTREAM_DOT_SERVER = { 1081 | hostname: config.DOT_SERVER_HOSTNAME || 'some-niche-dns.com', 1082 | port: config.DOT_SERVER_PORT || 853, 1083 | }; 1084 | } 1085 | 1086 | async handleDnsQuery(req) { 1087 | if (req.method !== 'POST' || req.headers.get('content-type') !== 'application/dns-message') { 1088 | return new Response('This is a DNS proxy. Please use a DoT client.', { status: 400 }); 1089 | } 1090 | 1091 | let clientDnsQuery; 1092 | try { 1093 | clientDnsQuery = await req.arrayBuffer(); 1094 | 1095 | const socket = connect(this.UPSTREAM_DOT_SERVER, { secureTransport: 'on', allowHalfOpen: false }); 1096 | const writer = socket.writable.getWriter(); 1097 | 1098 | const queryLength = clientDnsQuery.byteLength; 1099 | const lengthBuffer = new Uint8Array(2); 1100 | new DataView(lengthBuffer.buffer).setUint16(0, queryLength, false); 1101 | 1102 | const dotRequest = new Uint8Array(2 + queryLength); 1103 | dotRequest.set(lengthBuffer, 0); 1104 | dotRequest.set(new Uint8Array(clientDnsQuery), 2); 1105 | 1106 | await writer.write(dotRequest); 1107 | writer.releaseLock(); 1108 | 1109 | const reader = socket.readable.getReader(); 1110 | let responseChunks = []; 1111 | let totalLength = 0; 1112 | 1113 | while (true) { 1114 | const { done, value } = await reader.read(); 1115 | if (done) break; 1116 | responseChunks.push(value); 1117 | totalLength += value.length; 1118 | } 1119 | 1120 | reader.releaseLock(); 1121 | await socket.close(); 1122 | 1123 | const fullResponse = new Uint8Array(totalLength); 1124 | let offset = 0; 1125 | for (const chunk of responseChunks) { 1126 | fullResponse.set(chunk, offset); 1127 | offset += chunk.length; 1128 | } 1129 | 1130 | const responseLength = new DataView(fullResponse.buffer).getUint16(0, false); 1131 | const dnsResponse = fullResponse.slice(2, 2 + responseLength); 1132 | 1133 | return new Response(dnsResponse, { 1134 | headers: { 'content-type': 'application/dns-message' }, 1135 | }); 1136 | 1137 | } catch (socketError) { 1138 | this.log('DoT socket connection failed, falling back to DoH via fetch.', socketError); 1139 | 1140 | try { 1141 | this.log('Attempting DoH fallback...'); 1142 | const upstreamDnsUrl = `https://${this.config.DOH_SERVER_HOSTNAME}${this.config.DOH_SERVER_PATH || '/dns-query'}`; 1143 | 1144 | const dohHeaders = new Headers(); 1145 | dohHeaders.set("Host", this.config.DOH_SERVER_HOSTNAME); 1146 | dohHeaders.set("Content-Type", "application/dns-message"); 1147 | dohHeaders.set("Accept", "application/dns-message"); 1148 | 1149 | const fallbackRequest = new Request(upstreamDnsUrl, { 1150 | method: 'POST', 1151 | headers: dohHeaders, 1152 | body: clientDnsQuery, 1153 | }); 1154 | 1155 | return await fetch(fallbackRequest); 1156 | 1157 | } catch (fallbackError) { 1158 | this.log('DoH fallback also failed.', fallbackError); 1159 | return this.handleError(fallbackError, 'DoT and subsequent DoH fallback'); 1160 | } 1161 | } 1162 | } 1163 | 1164 | async connect(req, dstUrl) { 1165 | return await this.handleDnsQuery(req); 1166 | } 1167 | 1168 | async connectHttp(req, dstUrl) { 1169 | return await this.handleDnsQuery(req); 1170 | } 1171 | 1172 | async connectWebSocket(req, dstUrl) { 1173 | return new Response("DoT proxy does not support WebSocket", { status: 400 }); 1174 | } 1175 | } 1176 | 1177 | class ProxyFactory { 1178 | static createProxy(config) { 1179 | const strategy = config.PROXY_STRATEGY || 'socket'; 1180 | 1181 | switch (strategy.toLowerCase()) { 1182 | case 'socket': 1183 | return new SocketProxy(config); 1184 | case 'fetch': 1185 | return new FetchProxy(config); 1186 | case 'socks5': 1187 | return new Socks5Proxy(config); 1188 | case 'thirdparty': 1189 | return new ThirdPartyProxy(config); 1190 | case 'cloudprovider': 1191 | return new CloudProviderProxy(config); 1192 | case 'doh': 1193 | return new DoHProxy(config); 1194 | case 'dot': 1195 | return new DoTProxy(config); 1196 | default: 1197 | return new SocketProxy(config); 1198 | } 1199 | } 1200 | } 1201 | 1202 | class ShadowProxy { 1203 | static async handleRequest(req, env, ctx) { 1204 | try { 1205 | const config = ConfigManager.updateConfigFromEnv(env); 1206 | 1207 | const url = new URL(req.url); 1208 | const parts = url.pathname.split("/").filter(Boolean); 1209 | 1210 | if (parts.length >= 3 && parts[1] === 'dns') { 1211 | const auth = parts[0]; 1212 | const dnsType = parts[2]; 1213 | const server = parts[3]; 1214 | 1215 | if (auth === config.AUTH_TOKEN) { 1216 | let proxyStrategy = config.PROXY_STRATEGY; 1217 | if (dnsType === 'doh') { 1218 | proxyStrategy = 'doh'; 1219 | } else if (dnsType === 'dot') { 1220 | proxyStrategy = 'dot'; 1221 | } 1222 | 1223 | const dnsConfig = { ...config, PROXY_STRATEGY: proxyStrategy }; 1224 | const proxy = ProxyFactory.createProxy(dnsConfig); 1225 | 1226 | return await proxy.handleDnsQuery(req); 1227 | } 1228 | } 1229 | 1230 | const proxy = ProxyFactory.createProxy(config); 1231 | 1232 | const dstUrl = this.parseDestinationUrl(req, config); 1233 | 1234 | return await proxy.connect(req, dstUrl); 1235 | } catch (error) { 1236 | console.error("ShadowProxy error:", error); 1237 | return new Response(`Error: ${error.message}`, { status: 500 }); 1238 | } 1239 | } 1240 | 1241 | static parseDestinationUrl(req, config) { 1242 | const url = new URL(req.url); 1243 | const parts = url.pathname.split("/").filter(Boolean); 1244 | const [auth, protocol, ...path] = parts; 1245 | 1246 | const isValid = auth === config.AUTH_TOKEN; 1247 | 1248 | let dstUrl = config.DEFAULT_DST_URL; 1249 | 1250 | if (isValid && protocol) { 1251 | if (protocol.endsWith(':')) { 1252 | dstUrl = `${protocol}//${path.join("/")}${url.search}`; 1253 | } else { 1254 | dstUrl = `${protocol}://${path.join("/")}${url.search}`; 1255 | } 1256 | } 1257 | 1258 | if (config.DEBUG_MODE) { 1259 | console.log("Target URL", dstUrl); 1260 | } 1261 | 1262 | return dstUrl; 1263 | } 1264 | } 1265 | 1266 | export default { 1267 | async fetch(request, env, ctx) { 1268 | return await ShadowProxy.handleRequest(request, env, ctx); 1269 | } 1270 | }; 1271 | --------------------------------------------------------------------------------