const { EventEmitter } = require('events'); const debug = require('debug')('localtunnel:client'); const fs = require('fs'); const net = require('net'); const tls = require('tls'); const HeaderHostTransformer = require('./HeaderHostTransformer'); // 管理隧道集群 module.exports = class TunnelCluster extends EventEmitter { constructor(opts = {}) { super(opts); this.opts = opts; this.tunnelCount = 0; // 添加连接计数器 this.retryDelay = 1000; // 初始重试延迟 this.maxRetryDelay = 30000; // 最大重试延迟 debug('创建隧道集群, 配置:', { remote_host: opts.remote_host, remote_port: opts.remote_port, local_host: opts.local_host, local_port: opts.local_port }); } open() { const opt = this.opts; // 优先使用IP const remoteHostOrIp = opt.remote_ip || opt.remote_host; const remotePort = opt.remote_port; const localHost = opt.local_host || 'localhost'; const localPort = opt.local_port; const localProtocol = opt.local_https ? 'https' : 'http'; const allowInvalidCert = opt.allow_invalid_cert; debug( '开始建立隧道连接 %s://%s:%s <> %s:%s', localProtocol, localHost, localPort, remoteHostOrIp, remotePort ); // 连接到隧道服务器 const remote = net.connect({ host: remoteHostOrIp, port: remotePort, }); // 设置更长的超时时间 remote.setTimeout(30000); // 30秒超时 remote.setKeepAlive(true, 1000); debug('设置 TCP keepalive 和超时检测'); // 连接成功时增加计数 remote.on('connect', () => { this.tunnelCount++; debug('远程连接已建立 时间: %s [total: %d]', new Date().toISOString(), this.tunnelCount); }); // 连接关闭时减少计数 remote.on('close', (hadError) => { this.tunnelCount = Math.max(0, this.tunnelCount - 1); // 确保不会出现负数 debug('远程连接关闭 时间: %s [错误: %s] [total: %d]', new Date().toISOString(), hadError, this.tunnelCount ); // 使用指数退避策略重试 this.retryDelay = Math.min(this.retryDelay * 2, this.maxRetryDelay); debug('将在 %d ms 后重试', this.retryDelay); setTimeout(() => { this.emit('dead'); }, this.retryDelay); }); // 连接成功时重置重试延迟 remote.once('connect', () => { this.retryDelay = 1000; // 重置重试延迟 }); // 错误处理 remote.on('error', err => { debug('远程连接错误 时间: %s 错误: %s', new Date().toISOString(), err.stack || err.message ); if (err.code === 'ECONNREFUSED') { this.emit('error', new Error( `连接被拒绝: ${remoteHostOrIp}:${remotePort} (检查防火墙设置)` )); } // 不要立即销毁连接,让它自然关闭 remote.end(); }); // 监控远程连接状态 remote.on('connect', () => { debug('远程连接已建立 时间:', new Date().toISOString()); }); remote.on('timeout', () => { debug('远程连接超时 时间:', new Date().toISOString()); remote.destroy(new Error('连接超时')); }); remote.on('end', () => { debug('远程连接结束 时间:', new Date().toISOString()); }); // 添加心跳检测 let lastDataTime = Date.now(); const heartbeatInterval = 10000; // 10秒发送一次心跳 const heartbeatTimeout = 30000; // 30秒无响应才断开 // 发送心跳包 - 使用 HTTP OPTIONS 请求作为心跳 const heartbeatTimer = setInterval(() => { if (!remote.destroyed) { debug('发送心跳包 时间:', new Date().toISOString()); remote.write('OPTIONS /ping HTTP/1.1\r\nHost: ping\r\n\r\n'); } }, heartbeatInterval); // 心跳检测 const heartbeatCheck = setInterval(() => { const now = Date.now(); if (now - lastDataTime > heartbeatTimeout) { debug('心跳检测超时 时间:', new Date().toISOString()); remote.destroy(new Error('心跳检测失败')); clearInterval(heartbeatCheck); clearInterval(heartbeatTimer); } }, heartbeatInterval); // 接收数据时更新最后数据时间 remote.on('data', (data) => { lastDataTime = Date.now(); const dataStr = data.toString(); // 任何响应都视为有效心跳 if (dataStr.includes('HTTP/1.1')) { debug('收到服务器响应 时间:', new Date().toISOString()); } const match = dataStr.match(/^(\w+) (\S+)/); if (match) { debug('收到请求: %s %s', match[1], match[2]); this.emit('request', { method: match[1], path: match[2], }); } }); const connLocal = () => { if (remote.destroyed) { debug('远程连接已销毁,无法建立本地连接 时间:', new Date().toISOString()); clearInterval(heartbeatCheck); clearInterval(heartbeatTimer); this.emit('dead'); return; } debug('正在连接本地服务 %s://%s:%d', localProtocol, localHost, localPort); remote.pause(); if (allowInvalidCert) { debug('允许无效证书'); } // 创建本地连接 const local = opt.local_https ? tls.connect({ host: localHost, port: localPort, ...(allowInvalidCert ? { rejectUnauthorized: false } : { cert: fs.readFileSync(opt.local_cert), key: fs.readFileSync(opt.local_key), ca: opt.local_ca ? [fs.readFileSync(opt.local_ca)] : undefined, }), }) : net.connect({ host: localHost, port: localPort }); // 监控本地连接状态 local.on('connect', () => { debug('本地连接已建立 时间:', new Date().toISOString()); }); local.on('timeout', () => { debug('本地连接超时 时间:', new Date().toISOString()); local.end(); }); local.on('end', () => { debug('本地连接结束 时间:', new Date().toISOString()); }); const remoteClose = () => { debug('远程连接关闭,清理本地连接 时间:', new Date().toISOString()); this.emit('dead'); local.end(); }; remote.once('close', remoteClose); local.once('error', err => { debug('本地连接错误: %s 时间:', new Date().toISOString(), err.message); local.end(); remote.removeListener('close', remoteClose); if (err.code !== 'ECONNREFUSED' && err.code !== 'ECONNRESET') { debug('本地连接发生致命错误,关闭远程连接 时间:', new Date().toISOString()); return remote.end(); } debug('本地连接失败,1秒后重试 时间:', new Date().toISOString()); setTimeout(connLocal, 1000); }); local.once('connect', () => { debug('本地连接成功 时间:', new Date().toISOString()); remote.resume(); let stream = remote; // 如果指定了本地主机,使用 HeaderHostTransformer if (opt.local_host) { debug('转换 Host 头为 %s', opt.local_host); stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host })); } stream.pipe(local).pipe(remote); // 监控数据流 stream.on('data', (chunk) => { lastDataTime = Date.now(); // 更新最后数据时间 debug('收到数据 时间: %s 长度: %d', new Date().toISOString(), chunk.length); }); // 清理函数 const cleanup = () => { debug('清理资源 时间:', new Date().toISOString()); clearInterval(heartbeatCheck); clearInterval(heartbeatTimer); if (stream) { stream.unpipe(); } if (local) { local.unpipe(); } }; // 当连接关闭时清理资源 remote.once('close', cleanup); local.once('close', cleanup); }); }; // 监控请求数据 remote.on('data', data => { const match = data.toString().match(/^(\w+) (\S+)/); if (match) { debug('收到请求: %s %s', match[1], match[2]); this.emit('request', { method: match[1], path: match[2], }); } }); // 当远程连接建立时,认为隧道打开 remote.once('connect', () => { debug('远程连接成功,开始建立本地连接 时间:', new Date().toISOString()); this.emit('open', remote); connLocal(); }); } };