diff --git a/antigravity/node-tls-proxy/proxy.js b/antigravity/node-tls-proxy/proxy.js deleted file mode 100644 index 1b8f015e..00000000 --- a/antigravity/node-tls-proxy/proxy.js +++ /dev/null @@ -1,898 +0,0 @@ -'use strict'; - -const http = require('http'); -const https = require('https'); -const http2 = require('http2'); -const net = require('net'); -const crypto = require('crypto'); -// os 模块不引用 — 避免暴露真实主机信息 - -// ─── 配置 ─────────────────────────────────────────────── -const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com'; -const LISTEN_PORT = parseInt(process.env.PROXY_PORT || '3456', 10); -const LISTEN_HOST = process.env.PROXY_HOST || '127.0.0.1'; -const UPSTREAM_PROXY = process.env.UPSTREAM_PROXY || ''; -const CONNECT_TIMEOUT = parseInt(process.env.CONNECT_TIMEOUT || '30000', 10); -const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '600000', 10); -const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED !== 'false'; // 默认开启 -const DD_API_KEY = process.env.DD_API_KEY || 'pubbbf48e6d78dae54bceaa4acf463299bf'; -const CLI_VERSION = process.env.CLI_VERSION || '2.1.88'; -const BUILD_TIME = process.env.BUILD_TIME || '2026-03-31T01:39:46Z'; -// 伪装的 Node 版本(CLI 2.1.88 打包的 Bun 报告的 Node 兼容版本) -const FAKE_NODE_VERSION = process.env.FAKE_NODE_VERSION || 'v24.3.0'; - -const log = (level, msg, extra = {}) => { - const entry = { time: new Date().toISOString(), level, msg, ...extra }; - process.stderr.write(JSON.stringify(entry) + '\n'); -}; - -const HEALTH_PATH = '/__health'; -const h2Hosts = new Set(); - -// Strip userinfo (user:pass) from proxy URL for safe logging -function redactProxyURL(raw) { - if (!raw) return ''; - try { - const u = new URL(raw); - u.username = ''; - u.password = ''; - return u.toString(); - } catch { return ''; } -} -const h2Sessions = new Map(); - -// ─── 虚拟主机身份生成 ───────────────────────────────────── -// 每个账号基于 seed 生成全局唯一的主机身份,看起来像一台真实的个人开发机 -// 匹配 CLI 的 OTEL detectResources: hostDetector + processDetector + serviceInstanceIdDetector -// -// 设计原则: -// 1. 同一账号(seed)永远产出同一台"机器"的特征 -// 2. 不同账号的特征互不相同(无共享池、无碰撞) -// 3. 每个字段都像人手动设置的,不是程序生成的 - -// ─── macOS 主机身份词表 ────────────────────────────────────────── -// macOS 用户 hostname 习惯: "alex-MBP", "sam-MacBook-Pro" 等 -const MBP_NAMES = ['alex','sam','chris','max','lee','kai','jamie','taylor','morgan','casey', - 'drew','avery','riley','blake','jordan','ryan','parker','quinn','reese','cameron']; -const MBP_SUFFIX = ['-MBP','-MacBook','-MacBook-Pro','-MacBook-Air',"s-MBP","s-MacBook","s-MacBook-Pro"]; - -function generateHostIdentity(seed) { - const h = (s) => crypto.createHash('sha256').update(seed + ':' + s).digest(); - - // ── hostname: macOS 风格 ── - const hb = h('hostname'); - const name = MBP_NAMES[hb.readUInt8(0) % MBP_NAMES.length]; - const sfx = MBP_SUFFIX[hb.readUInt8(1) % MBP_SUFFIX.length]; - const hostname = `${name}${sfx}`; - - // ── username: 取自 hostname 名字(真实 Mac 行为) ── - const username = name; - - // ── terminal: macOS 常见终端分布 ── - const termRoll = h('terminal').readUInt8(0) % 100; - const terminal = termRoll < 75 ? 'xterm-256color' : - termRoll < 88 ? 'screen-256color' : - termRoll < 96 ? 'alacritty' : 'kitty'; - - // ── shell: macOS 默认 zsh(Catalina+);部分用 bash/fish ── - const shellRoll = h('shell').readUInt8(0) % 100; - const shell = shellRoll < 65 ? '/bin/zsh' : - shellRoll < 82 ? '/usr/local/bin/zsh' : - shellRoll < 93 ? '/bin/bash' : '/opt/homebrew/bin/fish'; - - // ── host.id: macOS IOPlatformUUID 格式(大写 UUID) ── - const mid = h('machine-id'); - const machineId = [ - mid.slice(0,4).toString('hex').toUpperCase(), - mid.slice(4,6).toString('hex').toUpperCase(), - mid.slice(6,8).toString('hex').toUpperCase(), - mid.slice(8,10).toString('hex').toUpperCase(), - mid.slice(10,16).toString('hex').toUpperCase(), - ].join('-'); - - // ── PID: macOS GUI 应用 PID 通常较小 ── - const pid = 500 + Math.floor(Math.random() * 8000); - - // ── macOS 版本: 13(Ventura)/14(Sonoma)/15(Sequoia) ── - const kb = h('kernel'); - const macosMajor = 13 + (kb.readUInt8(0) % 3); - const macosMinor = kb.readUInt8(1) % 8; - const macosPatch = kb.readUInt8(2) % 5; - // Darwin 内核: macOS 13=22.x, 14=23.x, 15=24.x - const darwinMajor = 22 + (macosMajor - 13); - const darwinMinor = kb.readUInt8(3) % 7; - const darwinPatch = kb.readUInt8(4) % 5; - const osVersion = `${macosMajor}.${macosMinor}.${macosPatch}`; - - // ── arch: Apple Silicon arm64 占 70%,Intel x64 占 30% ── - const arch = h('arch').readUInt8(0) % 100 < 70 ? 'arm64' : 'x64'; - - // ── 可执行文件路径: macOS 常见安装位置 ── - const pathRoll = h('execpath').readUInt8(0) % 100; - const executablePath = pathRoll < 50 ? `/Users/${username}/.claude/local/claude` : - pathRoll < 80 ? '/usr/local/bin/claude' : - pathRoll < 95 ? `/Users/${username}/.local/bin/claude` : - '/opt/homebrew/bin/claude'; - - return { - hostname, username, terminal, shell, machineId, pid, arch, - osType: 'Darwin', - osVersion, - kernelRelease: `${darwinMajor}.${darwinMinor}.${darwinPatch}`, - serviceInstanceId: crypto.randomUUID(), - executablePath, - executableName: 'claude', - command: 'claude', - commandArgs: [], - runtimeName: 'nodejs', - runtimeVersion: FAKE_NODE_VERSION.replace('v', ''), - ripgrepVersion: (() => { - const rv = h('ripgrep'); - return ['14.1.1','14.1.0','14.0.2','13.0.0','13.0.1','14.0.1','14.0.0'][rv.readUInt8(0) % 7]; - })(), - ripgrepPath: (() => { - const rp = h('rgpath'); - return [ - '/opt/homebrew/bin/rg', - '/usr/local/bin/rg', - `/Users/${username}/.cargo/bin/rg`, - '/usr/local/opt/ripgrep/bin/rg', - ][rp.readUInt8(0) % 4]; - })(), - mcpServerCount: 1 + (h('mcp').readUInt8(0) % 5), - mcpFailCount: h('mcp').readUInt8(1) % 3, - }; -} - -// ─── 遥测模拟 ──────────────────────────────────────────── - -// 每个 device_id 的会话状态 -const sessionStates = new Map(); - -function getOrCreateSession(deviceId) { - if (sessionStates.has(deviceId)) return sessionStates.get(deviceId); - const hostId = generateHostIdentity(deviceId); - const state = { - sessionId: crypto.randomUUID(), - deviceId, - hostId, - startTime: Date.now(), - requestCount: 0, - // 追踪 ripgrep 是否已上报 - ripgrepReported: false, - }; - sessionStates.set(deviceId, state); - return state; -} - -function generateDeviceId(accountSeed) { - return crypto.createHash('sha256').update(`device:${accountSeed}`).digest('hex'); -} - -// ─── OTEL Resource Attributes (匹配 CLI 的 detectResources) ─── - -function buildEnvBlock(hostId) { - const platformStr = 'darwin'; - return { - platform: platformStr, - node_version: FAKE_NODE_VERSION, - terminal: hostId.terminal, - package_managers: 'npm,pnpm', - runtimes: 'deno,node', - is_running_with_bun: true, - is_ci: false, - is_claubbit: false, - is_github_action: false, - is_claude_code_action: false, - is_claude_ai_auth: false, - version: CLI_VERSION, - arch: hostId.arch, - is_claude_code_remote: false, - deployment_environment: `unknown-${platformStr}`, - is_conductor: false, - version_base: CLI_VERSION, - build_time: BUILD_TIME, - is_local_agent_mode: false, - vcs: 'git', - platform_raw: platformStr, - }; -} - -function buildProcessMetrics(uptime) { - // 模拟真实 CLI 的内存曲线:RSS 随 uptime 缓慢增长 - const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000); - const rss = Math.floor(baseRss + Math.random() * 80_000_000); - const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000); - const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3); - return Buffer.from(JSON.stringify({ - uptime, - rss, - heapTotal, - heapUsed, - external: 14_000_000 + Math.floor(Math.random() * 2_000_000), - arrayBuffers: Math.floor(Math.random() * 200_000), - constrainedMemory: 51539607552, - cpuUsage: { - user: Math.floor(uptime * 10_000 + Math.random() * 300_000), - system: Math.floor(uptime * 2_000 + Math.random() * 80_000), - }, - cpuPercent: Math.random() * 200, - })).toString('base64'); -} - -function buildEvent(eventName, session, model, betas, extraData, timestampOverride) { - const uptime = (Date.now() - session.startTime) / 1000; - const processMetrics = buildProcessMetrics(uptime); - // 缓存最近一次的 process metrics,供 DataDog 日志复用(保持两边一致) - session._lastProcessMetrics = { uptime, raw: processMetrics }; - const eventData = { - event_name: eventName, - client_timestamp: timestampOverride || new Date().toISOString(), - model: model || 'claude-sonnet-4-6', - session_id: session.sessionId, - user_type: 'external', - betas: betas || 'claude-code-20250219,interleaved-thinking-2025-05-14', - env: buildEnvBlock(session.hostId), - entrypoint: 'cli', - is_interactive: true, - client_type: 'cli', - process: processMetrics, - event_id: crypto.randomUUID(), - device_id: session.deviceId, - // 注意:不加 resource 字段 — event_logging/batch 是自定义端点, - // OTEL resource attributes 由 CLI 通过单独的 OTLP exporter 发送,不在这里 - }; - // 合并额外字段(用于特定事件的附加数据) - if (extraData) Object.assign(eventData, extraData); - return { - event_type: 'ClaudeCodeInternalEvent', - event_data: eventData, - }; -} - -// 发送遥测到 api.anthropic.com/api/event_logging/batch -function sendTelemetryEvents(events, session) { - if (!TELEMETRY_ENABLED || events.length === 0) return; - - const body = JSON.stringify({ events }); - const headers = { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'User-Agent': `claude-code/${CLI_VERSION}`, - 'x-service-name': 'claude-code', - 'Content-Length': Buffer.byteLength(body), - }; - // 注意:真实 CLI 2.1.84 的 event_logging/batch 不发 traceparent - // traceparent 仅在 OTLP exporter(单独通道)中使用,不在这个端点 - - const opts = { - hostname: 'api.anthropic.com', - port: 443, - path: '/api/event_logging/batch', - method: 'POST', - headers, - timeout: 10000, - }; - - const req = https.request(opts, (res) => { - res.resume(); // drain - log('debug', 'telemetry_sent', { status: res.statusCode, events: events.length }); - }); - req.on('error', (err) => { - log('debug', 'telemetry_error', { error: err.message }); - }); - req.on('timeout', () => req.destroy()); - req.end(body); -} - -// 发送 DataDog 日志 -function sendDatadogLog(eventName, session, model) { - if (!TELEMETRY_ENABLED) return; - - const hostId = session.hostId; - const uptime = (Date.now() - session.startTime) / 1000; - - // 复用 Anthropic 事件侧缓存的 process metrics(保持两边数值一致) - // 如果没有缓存(首次调用),现场生成 - let pm; - if (session._lastProcessMetrics && Math.abs(session._lastProcessMetrics.uptime - uptime) < 2) { - pm = JSON.parse(Buffer.from(session._lastProcessMetrics.raw, 'base64').toString()); - } else { - const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000); - const rss = Math.floor(baseRss + Math.random() * 80_000_000); - const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000); - const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3); - pm = { - uptime, - rss, - heapTotal, - heapUsed, - external: 14_000_000 + Math.floor(Math.random() * 2_000_000), - arrayBuffers: Math.floor(Math.random() * 10_000), - constrainedMemory: 0, - cpuUsage: { - user: Math.floor(uptime * 10_000 + Math.random() * 300_000), - system: Math.floor(uptime * 2_000 + Math.random() * 80_000), - }, - }; - } - - const entry = { - ddsource: 'nodejs', - ddtags: `event:${eventName},arch:${hostId.arch},client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:darwin,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`, - message: eventName, - service: 'claude-code', - hostname: hostId.hostname, - env: 'external', - model: model || 'claude-sonnet-4-6', - session_id: session.sessionId, - user_type: 'external', - entrypoint: 'cli', - is_interactive: 'true', - client_type: 'cli', - process_metrics: pm, - platform: 'darwin', - platform_raw: 'darwin', - arch: hostId.arch, - node_version: FAKE_NODE_VERSION, - version: CLI_VERSION, - version_base: CLI_VERSION, - build_time: BUILD_TIME, - deployment_environment: 'unknown-darwin', - vcs: 'git', - }; - - const body = JSON.stringify([entry]); - const opts = { - hostname: 'http-intake.logs.us5.datadoghq.com', - port: 443, - path: '/api/v2/logs', - method: 'POST', - headers: { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'User-Agent': 'axios/1.13.6', - 'dd-api-key': DD_API_KEY, - 'Content-Length': Buffer.byteLength(body), - }, - timeout: 10000, - }; - - const req = https.request(opts, (res) => { res.resume(); }); - req.on('error', () => {}); - req.on('timeout', () => req.destroy()); - req.end(body); -} - -// 请求前发遥测(模拟 CLI 启动 + 初始化事件) -function emitPreRequestTelemetry(reqHeaders, body) { - const accountSeed = reqHeaders['x-forwarded-host'] || 'default'; - const deviceId = generateDeviceId(accountSeed + ':' + (reqHeaders['authorization'] || '').slice(-16)); - const session = getOrCreateSession(deviceId); - session.requestCount++; - - // 从请求体解析真实 model - let model = 'claude-sonnet-4-6'; - try { - const parsed = JSON.parse(body.toString()); - if (parsed.model) model = parsed.model; - } catch (_) {} - - const betas = reqHeaders['anthropic-beta'] || 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01'; - - // 首次请求:发完整启动事件序列(匹配真实 CLI 的时序) - if (session.requestCount === 1) { - const hostId = session.hostId; - // 生成递增的时间戳,模拟真实 CLI 启动流程的时间差 - const baseTime = Date.now(); - const ts = (offsetMs) => new Date(baseTime + offsetMs).toISOString(); - - // 第一批:启动 + 工具检测 + MCP 连接事件 - const batch1 = [ - buildEvent('tengu_started', session, model, betas, null, ts(0)), - buildEvent('tengu_init', session, model, betas, null, ts(80 + Math.floor(Math.random() * 120))), - // tengu_ripgrep_availability — CLI 必发的工具检测事件,版本/路径按账号不同 - buildEvent('tengu_ripgrep_availability', session, model, betas, { - ripgrep_available: true, - ripgrep_version: hostId.ripgrepVersion, - ripgrep_path: hostId.ripgrepPath, - }, ts(200 + Math.floor(Math.random() * 150))), - ]; - // MCP 连接事件:数量按账号不同(真实用户配置的 MCP server 数量差异很大) - let mcpOffset = 400; - const mcpSuccessCount = hostId.mcpServerCount - hostId.mcpFailCount; - for (let i = 0; i < hostId.mcpFailCount; i++) { - mcpOffset += 100 + Math.floor(Math.random() * 300); - batch1.push(buildEvent('tengu_mcp_server_connection_failed', session, model, betas, null, ts(mcpOffset))); - } - for (let i = 0; i < mcpSuccessCount; i++) { - mcpOffset += 200 + Math.floor(Math.random() * 500); - batch1.push(buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas, null, ts(mcpOffset))); - } - - session.ripgrepReported = true; - sendTelemetryEvents(batch1, session); - sendDatadogLog('tengu_started', session, model); - sendDatadogLog('tengu_init', session, model); - - // 第二批延迟发送(真实 CLI 间隔约 30 秒) - setTimeout(() => { - const batch2 = [ - buildEvent('tengu_session_init', session, model, betas), - buildEvent('tengu_context_loaded', session, model, betas), - ]; - sendTelemetryEvents(batch2, session); - }, 25000 + Math.floor(Math.random() * 10000)); - } - - // 每次请求:发 request_started - const events = [ - buildEvent('tengu_api_request_started', session, model, betas), - ]; - sendTelemetryEvents(events, session); -} - -// 请求后发遥测 -function emitPostRequestTelemetry(reqHeaders, statusCode, body) { - const accountSeed = reqHeaders['x-forwarded-host'] || 'default'; - const deviceId = generateDeviceId(accountSeed + ':' + (reqHeaders['authorization'] || '').slice(-16)); - const session = getOrCreateSession(deviceId); - - let model = 'claude-sonnet-4-6'; - try { - const parsed = JSON.parse(body.toString()); - if (parsed.model) model = parsed.model; - } catch (_) {} - - const betas = reqHeaders['anthropic-beta'] || 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24,token-efficient-tools-2026-03-28,advisor-tool-2026-03-01'; - - // 请求完成事件 - const events = [ - buildEvent('tengu_api_request_completed', session, model, betas), - buildEvent('tengu_conversation_turn_completed', session, model, betas), - ]; - sendTelemetryEvents(events, session); - sendDatadogLog('tengu_api_request_completed', session, model); - - // 模拟错误遥测(低概率,匹配 TelemetrySafeError) - if (statusCode >= 400 && Math.random() < 0.5) { - const errorEvent = buildEvent('tengu_api_request_error', session, model, betas, { - error_type: 'TelemetrySafeError', - error_code: statusCode, - error_message: statusCode === 429 ? 'rate_limit_exceeded' : - statusCode === 529 ? 'overloaded' : - statusCode >= 500 ? 'server_error' : 'client_error', - }); - sendTelemetryEvents([errorEvent], session); - } - - // 随机发额外事件(仅使用已知的真实 CLI 事件名) - if (Math.random() < 0.3) { - setTimeout(() => { - const extra = [ - buildEvent('tengu_tool_use_completed', session, model, betas), - ]; - sendTelemetryEvents(extra, session); - }, 2000 + Math.floor(Math.random() * 5000)); - } -} - -// ─── H2 session 管理 ──────────────────────────────────── -// h2Sessions key 改为 host+proxy 组合,避免不同代理的 session 混用 -function h2SessionKey(host, proxyUrl) { - return proxyUrl ? `${host}|${proxyUrl}` : host; -} - -async function getOrCreateH2Session(host, proxyUrl) { - const key = h2SessionKey(host, proxyUrl); - const existing = h2Sessions.get(key); - // 检查 session 是否仍然可用:connected 且未关闭 - // GOAWAY 后 session.connected 变为 false,必须重建 - if (existing && !existing.closed && !existing.destroyed && existing.connected) return existing; - if (existing) { - h2Sessions.delete(key); - try { existing.close(); } catch (_) {} - } - - let session; - if (proxyUrl) { - // 通过 CONNECT 隧道建立 h2 session(支持 HTTP CONNECT / SOCKS5) - const socket = await connectViaProxy(proxyUrl, host, 443); - session = http2.connect(`https://${host}`, { - createConnection: () => socket, - }); - log('info', 'h2_session_via_proxy', { host, proxy: redactProxyURL(proxyUrl) }); - } else { - session = http2.connect(`https://${host}`); - } - - session.on('error', (err) => { - log('warn', 'h2_session_error', { host, error: err.message }); - h2Sessions.delete(key); - try { session.close(); } catch (_) {} - }); - session.on('close', () => h2Sessions.delete(key)); - session.on('goaway', (errorCode) => { - log('info', 'h2_goaway', { host, errorCode }); - h2Sessions.delete(key); - try { session.close(); } catch (_) {} - }); - session.setTimeout(IDLE_TIMEOUT, () => { session.close(); h2Sessions.delete(key); }); - h2Sessions.set(key, session); - return session; -} - -function waitForConnect(session) { - if (session.connected) return Promise.resolve(); - // session 已断开(GOAWAY / 半关闭),不要等不会来的 connect 事件 - if (session.closed || session.destroyed) { - return Promise.reject(new Error('h2 session already closed')); - } - return new Promise((resolve, reject) => { - const onConnect = () => { clearTimeout(t); cleanup(); resolve(); }; - const onError = (err) => { clearTimeout(t); cleanup(); reject(err); }; - const onClose = () => { clearTimeout(t); cleanup(); reject(new Error('h2 session closed before connect')); }; - const cleanup = () => { - session.removeListener('connect', onConnect); - session.removeListener('error', onError); - session.removeListener('close', onClose); - }; - session.once('connect', onConnect); - session.once('error', onError); - session.once('close', onClose); - const t = setTimeout(() => { cleanup(); reject(new Error('h2 connect timeout')); }, CONNECT_TIMEOUT); - }); -} - -// ─── CONNECT 隧道(HTTP CONNECT + SOCKS5)───────────────── -function connectViaProxy(proxyUrl, targetHost, targetPort) { - const proxy = new URL(proxyUrl); - const scheme = proxy.protocol.replace(':', '').toLowerCase(); - - if (scheme === 'socks5' || scheme === 'socks5h') { - return connectViaSocks5(proxy, targetHost, parseInt(targetPort, 10)); - } - return connectViaHttpConnect(proxy, targetHost, targetPort); -} - -// HTTP CONNECT 隧道 -function connectViaHttpConnect(proxy, targetHost, targetPort) { - return new Promise((resolve, reject) => { - const conn = net.connect(parseInt(proxy.port || '80', 10), proxy.hostname, () => { - const auth = proxy.username - ? `Proxy-Authorization: Basic ${Buffer.from(`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password || '')}`).toString('base64')}\r\n` - : ''; - conn.write(`CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}\r\n`); - }); - conn.once('error', reject); - conn.setTimeout(CONNECT_TIMEOUT, () => conn.destroy(new Error('CONNECT timeout'))); - let buf = ''; - conn.on('data', function onData(chunk) { - buf += chunk.toString(); - const idx = buf.indexOf('\r\n\r\n'); - if (idx === -1) return; - conn.removeListener('data', onData); - const code = parseInt(buf.split(' ')[1], 10); - if (code === 200) { conn.setTimeout(0); resolve(conn); } - else { conn.destroy(); reject(new Error(`CONNECT ${code}`)); } - }); - }); -} - -// SOCKS5 隧道 (RFC 1928 + RFC 1929 username/password auth) -function connectViaSocks5(proxy, targetHost, targetPort) { - return new Promise((resolve, reject) => { - const conn = net.connect(parseInt(proxy.port || '1080', 10), proxy.hostname); - conn.once('error', reject); - conn.setTimeout(CONNECT_TIMEOUT, () => conn.destroy(new Error('SOCKS5 timeout'))); - - const username = proxy.username ? decodeURIComponent(proxy.username) : ''; - const password = proxy.password ? decodeURIComponent(proxy.password) : ''; - const useAuth = !!(username || password); - - let step = 'greeting'; - - conn.once('connect', () => { - // Step 1: 发送 greeting — 支持的认证方式 - // 0x00 = 无认证, 0x02 = 用户名/密码 - if (useAuth) { - conn.write(Buffer.from([0x05, 0x02, 0x00, 0x02])); - } else { - conn.write(Buffer.from([0x05, 0x01, 0x00])); - } - }); - - let pending = Buffer.alloc(0); - conn.on('data', function onData(chunk) { - pending = Buffer.concat([pending, chunk]); - - if (step === 'greeting') { - if (pending.length < 2) return; - const ver = pending[0], method = pending[1]; - if (ver !== 0x05) { conn.destroy(); return reject(new Error(`SOCKS5 bad version: ${ver}`)); } - - if (method === 0x02 && useAuth) { - // Step 2: 用户名/密码认证 (RFC 1929) - step = 'auth'; - pending = pending.slice(2); - const uBuf = Buffer.from(username, 'utf8'); - const pBuf = Buffer.from(password, 'utf8'); - const authBuf = Buffer.alloc(3 + uBuf.length + pBuf.length); - authBuf[0] = 0x01; // auth version - authBuf[1] = uBuf.length; - uBuf.copy(authBuf, 2); - authBuf[2 + uBuf.length] = pBuf.length; - pBuf.copy(authBuf, 3 + uBuf.length); - conn.write(authBuf); - } else if (method === 0x00) { - // 无需认证,直接发 CONNECT - step = 'connect'; - pending = pending.slice(2); - sendSocks5Connect(conn, targetHost, targetPort); - } else { - conn.destroy(); - reject(new Error(`SOCKS5 unsupported auth method: ${method}`)); - } - } else if (step === 'auth') { - if (pending.length < 2) return; - const status = pending[1]; - if (status !== 0x00) { conn.destroy(); return reject(new Error(`SOCKS5 auth failed: ${status}`)); } - step = 'connect'; - pending = pending.slice(2); - sendSocks5Connect(conn, targetHost, targetPort); - } else if (step === 'connect') { - // 最小响应: VER(1) + REP(1) + RSV(1) + ATYP(1) + ADDR(variable) + PORT(2) - if (pending.length < 4) return; - const rep = pending[1]; - const atyp = pending[3]; - let minLen = 4 + 2; // base + port - if (atyp === 0x01) minLen += 4; // IPv4 - else if (atyp === 0x04) minLen += 16; // IPv6 - else if (atyp === 0x03 && pending.length > 4) minLen += 1 + pending[4]; // domain - else if (atyp === 0x03) return; // 等更多数据 - if (pending.length < minLen) return; - - conn.removeListener('data', onData); - if (rep !== 0x00) { conn.destroy(); return reject(new Error(`SOCKS5 connect failed: rep=${rep}`)); } - conn.setTimeout(0); - resolve(conn); - } - }); - }); -} - -function sendSocks5Connect(conn, host, port) { - // SOCKS5 CONNECT: VER(05) CMD(01=CONNECT) RSV(00) ATYP ADDR PORT - const hostBuf = Buffer.from(host, 'utf8'); - const buf = Buffer.alloc(4 + 1 + hostBuf.length + 2); - buf[0] = 0x05; // version - buf[1] = 0x01; // CONNECT - buf[2] = 0x00; // reserved - buf[3] = 0x03; // domain name - buf[4] = hostBuf.length; - hostBuf.copy(buf, 5); - buf.writeUInt16BE(port, 5 + hostBuf.length); - conn.write(buf); -} - -// ─── 收集请求体 ────────────────────────────────────────── -function collectBody(req) { - return new Promise((resolve) => { - const chunks = []; - req.on('data', (c) => chunks.push(c)); - req.on('end', () => resolve(Buffer.concat(chunks))); - req.on('error', () => resolve(Buffer.concat(chunks))); - }); -} - -// ─── H1 代理 ───────────────────────────────────────────── -function sendViaH1(targetHost, method, path, reqHeaders, body, res, savedHeaders, explicitProxy) { - return new Promise((resolve) => { - const headers = { ...reqHeaders, host: targetHost }; - ['x-forwarded-host', 'connection', 'keep-alive', 'proxy-connection', 'transfer-encoding'].forEach(h => delete headers[h]); - delete headers['x-upstream-proxy']; - if (body.length > 0) headers['content-length'] = String(body.length); - - const opts = { hostname: targetHost, port: 443, path, method, headers, servername: targetHost, timeout: CONNECT_TIMEOUT }; - const startTime = Date.now(); - - const finish = (requestOpts) => { - const proxyReq = https.request(requestOpts); - proxyReq.on('response', (proxyRes) => { - log('info', 'proxy_response', { host: targetHost, status: proxyRes.statusCode, path, proto: 'h1' }); - const rh = { ...proxyRes.headers }; - delete rh['connection']; delete rh['keep-alive']; - res.writeHead(proxyRes.statusCode, rh); - proxyRes.pipe(res, { end: true }); - // 请求完成后发遥测 - if (path.includes('/v1/messages') && savedHeaders) { - emitPostRequestTelemetry(savedHeaders, proxyRes.statusCode, body); - } - resolve('ok'); - }); - proxyReq.on('error', (err) => { - if (err.message === 'socket hang up' && (Date.now() - startTime) < 2000) { - log('info', 'h1_rejected_switching_to_h2', { host: targetHost }); - h2Hosts.add(targetHost); - sendViaH2(targetHost, method, path, reqHeaders, body, res, savedHeaders, false, explicitProxy).then(() => resolve('h2')); - return; - } - log('error', 'h1_error', { error: err.message, host: targetHost, path }); - if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: 'upstream_connection_error' })); } - resolve('error'); - }); - proxyReq.on('timeout', () => proxyReq.destroy(new Error('timeout'))); - proxyReq.end(body); - }; - - // 动态上游代理:使用显式传入的代理地址 - const upstreamProxy = explicitProxy || ''; - - if (upstreamProxy) { - connectViaProxy(upstreamProxy, targetHost, 443) - .then((socket) => { opts.socket = socket; opts.agent = false; finish(opts); }) - .catch((err) => { log('error', 'tunnel_failed', { error: err.message, proxy: redactProxyURL(upstreamProxy) }); if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: 'upstream_connection_error' })); } resolve('error'); }); - } else { - finish(opts); - } - }); -} - -// ─── H2 代理 ───────────────────────────────────────────── -async function sendViaH2(targetHost, method, path, reqHeaders, body, res, savedHeaders, _retried, proxyUrl) { - try { - const session = await getOrCreateH2Session(targetHost, proxyUrl); - await waitForConnect(session); - - const headers = {}; - const skip = new Set(['host','connection','keep-alive','proxy-connection','transfer-encoding','upgrade','x-forwarded-host','http2-settings']); - for (const [k, v] of Object.entries(reqHeaders)) { - if (!skip.has(k.toLowerCase())) headers[k] = v; - } - headers[':method'] = method; - headers[':path'] = path; - headers[':authority'] = targetHost; - headers[':scheme'] = 'https'; - if (body.length > 0) headers['content-length'] = String(body.length); - - const stream = session.request(headers); - let responded = false; - - stream.on('response', (h2h) => { - responded = true; - const status = h2h[':status'] || 502; - const rh = {}; - for (const [k, v] of Object.entries(h2h)) { if (!k.startsWith(':')) rh[k] = v; } - log('info', 'proxy_response', { host: targetHost, status, path, proto: 'h2' }); - res.writeHead(status, rh); - stream.on('data', (c) => res.write(c)); - stream.on('end', () => res.end()); - if (path.includes('/v1/messages') && savedHeaders) { - emitPostRequestTelemetry(savedHeaders, status); - } - }); - - stream.on('error', (err) => { - if (err.message && err.message.includes('NGHTTP2')) { - h2Sessions.delete(targetHost); - try { session.close(); } catch (_) {} - } - if (responded) { if (!res.writableEnded) res.end(); return; } - log('error', 'h2_error', { error: err.message, host: targetHost, path }); - if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: 'upstream_connection_error' })); } - }); - - stream.on('close', () => { - if (!responded && !res.headersSent) { - log('warn', 'h2_no_response', { host: targetHost, path }); - res.writeHead(502); res.end(JSON.stringify({ error: 'upstream_connection_error' })); - } else if (!res.writableEnded) { res.end(); } - }); - - stream.setTimeout(CONNECT_TIMEOUT, () => stream.close()); - stream.end(body); - } catch (err) { - log('error', 'h2_exception', { error: err.message, host: targetHost, retried: !!_retried }); - h2Sessions.delete(targetHost); - // 首次失败时重试一次(用全新 session) - if (!_retried && !res.headersSent) { - log('info', 'h2_retry_with_fresh_session', { host: targetHost, path }); - return sendViaH2(targetHost, method, path, reqHeaders, body, res, savedHeaders, true, proxyUrl); - } - if (!res.headersSent) { res.writeHead(502); res.end(JSON.stringify({ error: err.message })); } - } -} - -// ─── 请求入口 ───────────────────────────────────────────── -async function proxyRequest(req, res) { - const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST; - log('info', 'proxy_request', { host: targetHost, method: req.method, path: req.url }); - - // 保存原始 headers 用于遥测 - const savedHeaders = { ...req.headers }; - - const body = await collectBody(req); - - // 请求前发遥测(仅 /v1/messages 请求) - if (req.url.includes('/v1/messages') && TELEMETRY_ENABLED) { - emitPreRequestTelemetry(savedHeaders, body); - } - - // ── Jitter 注入 ────────────────────────────────────────────────── - // 模拟人类编码间歇:80% 快速响应(80-300ms),20% 慢速思考(400-1200ms) - // 使用 -log(rand) 指数衰减使延迟尾部更接近真实键盘输入节奏 - const jitterMs = (() => { - if (Math.random() < 0.80) { - return Math.floor(80 + (-Math.log(Math.random()) * 90)); // 快:~80-300ms - } - return Math.floor(400 + Math.random() * 800); // 慢:400-1200ms - })(); - await new Promise(r => setTimeout(r, jitterMs)); - - // ── H2 / H1 路由策略 ────────────────────────────────────────────── - // H2 现在支持通过 CONNECT 隧道代理,优先为 H2_PREFER_HOSTS 使用 h2。 - // 有代理时通过 connectViaProxy 建立隧道后再 h2 连接。 - const upstreamProxy = req.headers['x-upstream-proxy'] || UPSTREAM_PROXY; - // 清除内部 header,不传给上游(h2 路径也需要清理) - delete req.headers['x-upstream-proxy']; - const H2_PREFER_HOSTS = new Set([ - 'api.anthropic.com', - 'cloudaicompanion.googleapis.com', - 'generativelanguage.googleapis.com', - 'cloudcode-pa.googleapis.com', - 'daily-cloudcode-pa.googleapis.com', - ]); - if (H2_PREFER_HOSTS.has(targetHost) || h2Hosts.has(targetHost)) { - await sendViaH2(targetHost, req.method, req.url, req.headers, body, res, savedHeaders, false, upstreamProxy || undefined); - } else { - await sendViaH1(targetHost, req.method, req.url, req.headers, body, res, savedHeaders, upstreamProxy || undefined); - } -} - -// ─── HTTP 服务器 ───────────────────────────────────────── -const server = http.createServer((req, res) => { - if (req.url === HEALTH_PATH) { - res.writeHead(200, { 'content-type': 'application/json' }); - res.end(JSON.stringify({ - status: 'ok', node: process.version, openssl: process.versions.openssl, - uptime: process.uptime(), h2Hosts: [...h2Hosts], - telemetry: TELEMETRY_ENABLED, sessions: sessionStates.size, - })); - return; - } - proxyRequest(req, res).catch((err) => { - log('error', 'unhandled', { error: err.message }); - if (!res.headersSent) { res.writeHead(500); res.end('internal error'); } - }); -}); - -server.timeout = 0; -server.keepAliveTimeout = IDLE_TIMEOUT; -server.headersTimeout = 60000; -server.listen(LISTEN_PORT, LISTEN_HOST, () => { - log('info', 'node-tls-proxy started', { - listen: `${LISTEN_HOST}:${LISTEN_PORT}`, node: process.version, openssl: process.versions.openssl, - telemetry: TELEMETRY_ENABLED, - }); -}); - -// 定期清理过期 session(1 小时无活动) -setInterval(() => { - const now = Date.now(); - for (const [id, state] of sessionStates) { - if (now - state.startTime > 3600_000) sessionStates.delete(id); - } -}, 300_000); - -let stopping = false; -function shutdown(sig) { - if (stopping) return; stopping = true; - for (const s of h2Sessions.values()) try { s.close(); } catch (_) {} - h2Sessions.clear(); - server.close(() => process.exit(0)); - setTimeout(() => process.exit(1), 5000); -} -process.on('SIGTERM', () => shutdown('SIGTERM')); -process.on('SIGINT', () => shutdown('SIGINT')); -process.on('uncaughtException', (e) => log('error', 'uncaught', { error: e.message })); -process.on('unhandledRejection', (r) => log('error', 'rejection', { error: String(r) }));