AIClient-2-API/src/utils/tls-sidecar.js
hex2077 81dd6a3f86 feat(tls): 添加 Go uTLS sidecar 以绕过 Cloudflare TLS 指纹检测
- 新增 Go 语言编写的 TLS sidecar 服务,使用 uTLS 库模拟 Chrome 指纹
- 在 Dockerfile 中添加多阶段构建以编译 sidecar 二进制文件
- 扩展配置系统,支持启用/禁用 sidecar 及自定义端口
- 修改 Grok 提供商,使其请求可通过 sidecar 转发
- 在前端界面添加 TLS sidecar 配置选项和国际化支持
- 服务启动时自动启动 sidecar,关闭时优雅停止
2026-02-28 00:02:12 +08:00

288 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TLS Sidecar Manager
*
* 管理 Go uTLS sidecar 进程的生命周期:
* - 启动/停止 sidecar 二进制
* - 健康检查 & 自动重启
* - 为 axios 提供 sidecar 代理配置
*/
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import logger from './logger.js';
import http from 'http';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_PORT = 9090;
const HEALTH_CHECK_INTERVAL = 30000; // 30s
const HEALTH_CHECK_TIMEOUT = 3000; // 3s
const MAX_RESTART_ATTEMPTS = 5;
const RESTART_DELAY = 2000; // 2s
class TLSSidecar {
constructor() {
this.process = null;
this.port = DEFAULT_PORT;
this.baseUrl = null;
this.healthCheckTimer = null;
this.restartCount = 0;
this.isShuttingDown = false;
this.ready = false;
}
/**
* 启动 sidecar 进程
* @param {Object} options
* @param {number} [options.port] - 监听端口
* @param {string} [options.binaryPath] - 自定义二进制路径
* @returns {Promise<boolean>}
*/
async start(options = {}) {
if (this.process) {
logger.info('[TLS-Sidecar] Already running');
return true;
}
this.port = options.port || parseInt(process.env.TLS_SIDECAR_PORT) || DEFAULT_PORT;
this.baseUrl = `http://127.0.0.1:${this.port}`;
// 查找二进制文件
const binaryPath = options.binaryPath || this._findBinary();
if (!binaryPath) {
logger.error('[TLS-Sidecar] Binary not found. Build it with: cd tls-sidecar && go build -o tls-sidecar');
return false;
}
logger.info(`[TLS-Sidecar] Starting: ${binaryPath} on port ${this.port}`);
try {
this.process = spawn(binaryPath, [], {
env: {
...process.env,
TLS_SIDECAR_PORT: String(this.port),
},
stdio: ['ignore', 'pipe', 'pipe'],
});
// 转发 sidecar 日志
this.process.stdout.on('data', (data) => {
const msg = data.toString().trim();
if (msg) logger.info(`[TLS-Sidecar] ${msg}`);
});
this.process.stderr.on('data', (data) => {
const msg = data.toString().trim();
if (msg) logger.error(`[TLS-Sidecar] ${msg}`);
});
this.process.on('exit', (code, signal) => {
logger.warn(`[TLS-Sidecar] Process exited (code=${code}, signal=${signal})`);
this.process = null;
this.ready = false;
if (!this.isShuttingDown && this.restartCount < MAX_RESTART_ATTEMPTS) {
this.restartCount++;
logger.info(`[TLS-Sidecar] Auto-restart attempt ${this.restartCount}/${MAX_RESTART_ATTEMPTS}`);
setTimeout(() => this.start(options), RESTART_DELAY);
}
});
this.process.on('error', (err) => {
logger.error(`[TLS-Sidecar] Spawn error: ${err.message}`);
this.process = null;
this.ready = false;
});
// 等待 sidecar 就绪
const ok = await this._waitForReady();
if (ok) {
this.ready = true;
this.restartCount = 0;
this._startHealthCheck();
logger.info(`[TLS-Sidecar] Ready at ${this.baseUrl}`);
}
return ok;
} catch (err) {
logger.error(`[TLS-Sidecar] Failed to start: ${err.message}`);
return false;
}
}
/**
* 停止 sidecar 进程
*/
async stop() {
this.isShuttingDown = true;
this._stopHealthCheck();
if (this.process) {
logger.info('[TLS-Sidecar] Stopping...');
return new Promise((resolve) => {
const timeout = setTimeout(() => {
if (this.process) {
logger.warn('[TLS-Sidecar] Force killing');
this.process.kill('SIGKILL');
}
resolve();
}, 5000);
this.process.once('exit', () => {
clearTimeout(timeout);
this.process = null;
this.ready = false;
logger.info('[TLS-Sidecar] Stopped');
resolve();
});
this.process.kill('SIGTERM');
});
}
}
/**
* 检查 sidecar 是否正在运行且健康
* @returns {boolean}
*/
isReady() {
return this.ready && this.process !== null;
}
/**
* 获取 sidecar base URL
* @returns {string|null}
*/
getBaseUrl() {
return this.isReady() ? this.baseUrl : null;
}
/**
* 为 axios 配置 sidecar 代理
* 将目标 URL 改为 sidecar 地址,原始目标通过 header 传递
*
* @param {Object} axiosConfig - axios 配置对象
* @param {string} [proxyUrl] - 上游代理 URL可选
* @returns {Object} 修改后的 axios 配置
*/
wrapAxiosConfig(axiosConfig, proxyUrl) {
if (!this.isReady()) {
return axiosConfig; // sidecar 不可用,原样返回
}
const targetUrl = axiosConfig.url;
// 将请求指向 sidecar
axiosConfig.url = this.baseUrl;
// 通过 header 传递目标和代理信息
axiosConfig.headers = axiosConfig.headers || {};
axiosConfig.headers['X-Target-Url'] = targetUrl;
if (proxyUrl) {
axiosConfig.headers['X-Proxy-Url'] = proxyUrl;
}
// 走 sidecar 不需要 Node.js 侧的 TLS agent
delete axiosConfig.httpAgent;
delete axiosConfig.httpsAgent;
// 确保 axios 不使用自己的代理
axiosConfig.proxy = false;
return axiosConfig;
}
// ──── 内部方法 ────
_findBinary() {
const projectRoot = path.resolve(__dirname, '..', '..');
const isWin = process.platform === 'win32';
const ext = isWin ? '.exe' : '';
const candidates = [
path.join(projectRoot, 'tls-sidecar', `tls-sidecar${ext}`),
path.join(projectRoot, `tls-sidecar${ext}`),
path.join('/usr', 'local', 'bin', `tls-sidecar${ext}`),
path.join('/app', 'tls-sidecar', `tls-sidecar${ext}`),
path.join('/app', `tls-sidecar${ext}`),
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) {
return p;
}
} catch { /* ignore */ }
}
return null;
}
async _waitForReady(timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const ok = await this._healthCheck();
if (ok) return true;
} catch { /* retry */ }
await sleep(500);
}
logger.error('[TLS-Sidecar] Timed out waiting for sidecar to become ready');
return false;
}
_healthCheck() {
return new Promise((resolve) => {
const req = http.get(`${this.baseUrl}/health`, { timeout: HEALTH_CHECK_TIMEOUT }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
resolve(res.statusCode === 200);
});
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
_startHealthCheck() {
this._stopHealthCheck();
this.healthCheckTimer = setInterval(async () => {
const ok = await this._healthCheck();
if (!ok && this.ready) {
logger.warn('[TLS-Sidecar] Health check failed');
this.ready = false;
} else if (ok && !this.ready) {
logger.info('[TLS-Sidecar] Recovered');
this.ready = true;
}
}, HEALTH_CHECK_INTERVAL);
}
_stopHealthCheck() {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = null;
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 单例
let instance = null;
export function getTLSSidecar() {
if (!instance) {
instance = new TLSSidecar();
}
return instance;
}
export default TLSSidecar;