| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- 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();
- });
- }
- };
|