TunnelCluster.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. const { EventEmitter } = require('events');
  2. const debug = require('debug')('localtunnel:client');
  3. const fs = require('fs');
  4. const net = require('net');
  5. const tls = require('tls');
  6. const HeaderHostTransformer = require('./HeaderHostTransformer');
  7. // 管理隧道集群
  8. module.exports = class TunnelCluster extends EventEmitter {
  9. constructor(opts = {}) {
  10. super(opts);
  11. this.opts = opts;
  12. this.tunnelCount = 0; // 添加连接计数器
  13. this.retryDelay = 1000; // 初始重试延迟
  14. this.maxRetryDelay = 30000; // 最大重试延迟
  15. debug('创建隧道集群, 配置:', {
  16. remote_host: opts.remote_host,
  17. remote_port: opts.remote_port,
  18. local_host: opts.local_host,
  19. local_port: opts.local_port
  20. });
  21. }
  22. open() {
  23. const opt = this.opts;
  24. // 优先使用IP
  25. const remoteHostOrIp = opt.remote_ip || opt.remote_host;
  26. const remotePort = opt.remote_port;
  27. const localHost = opt.local_host || 'localhost';
  28. const localPort = opt.local_port;
  29. const localProtocol = opt.local_https ? 'https' : 'http';
  30. const allowInvalidCert = opt.allow_invalid_cert;
  31. debug(
  32. '开始建立隧道连接 %s://%s:%s <> %s:%s',
  33. localProtocol,
  34. localHost,
  35. localPort,
  36. remoteHostOrIp,
  37. remotePort
  38. );
  39. // 连接到隧道服务器
  40. const remote = net.connect({
  41. host: remoteHostOrIp,
  42. port: remotePort,
  43. });
  44. // 设置更长的超时时间
  45. remote.setTimeout(30000); // 30秒超时
  46. remote.setKeepAlive(true, 1000);
  47. debug('设置 TCP keepalive 和超时检测');
  48. // 连接成功时增加计数
  49. remote.on('connect', () => {
  50. this.tunnelCount++;
  51. debug('远程连接已建立 时间: %s [total: %d]', new Date().toISOString(), this.tunnelCount);
  52. });
  53. // 连接关闭时减少计数
  54. remote.on('close', (hadError) => {
  55. this.tunnelCount = Math.max(0, this.tunnelCount - 1); // 确保不会出现负数
  56. debug('远程连接关闭 时间: %s [错误: %s] [total: %d]',
  57. new Date().toISOString(),
  58. hadError,
  59. this.tunnelCount
  60. );
  61. // 使用指数退避策略重试
  62. this.retryDelay = Math.min(this.retryDelay * 2, this.maxRetryDelay);
  63. debug('将在 %d ms 后重试', this.retryDelay);
  64. setTimeout(() => {
  65. this.emit('dead');
  66. }, this.retryDelay);
  67. });
  68. // 连接成功时重置重试延迟
  69. remote.once('connect', () => {
  70. this.retryDelay = 1000; // 重置重试延迟
  71. });
  72. // 错误处理
  73. remote.on('error', err => {
  74. debug('远程连接错误 时间: %s 错误: %s',
  75. new Date().toISOString(),
  76. err.stack || err.message
  77. );
  78. if (err.code === 'ECONNREFUSED') {
  79. this.emit('error', new Error(
  80. `连接被拒绝: ${remoteHostOrIp}:${remotePort} (检查防火墙设置)`
  81. ));
  82. }
  83. // 不要立即销毁连接,让它自然关闭
  84. remote.end();
  85. });
  86. // 监控远程连接状态
  87. remote.on('connect', () => {
  88. debug('远程连接已建立 时间:', new Date().toISOString());
  89. });
  90. remote.on('timeout', () => {
  91. debug('远程连接超时 时间:', new Date().toISOString());
  92. remote.destroy(new Error('连接超时'));
  93. });
  94. remote.on('end', () => {
  95. debug('远程连接结束 时间:', new Date().toISOString());
  96. });
  97. // 添加心跳检测
  98. let lastDataTime = Date.now();
  99. const heartbeatInterval = 10000; // 10秒发送一次心跳
  100. const heartbeatTimeout = 30000; // 30秒无响应才断开
  101. // 发送心跳包 - 使用 HTTP OPTIONS 请求作为心跳
  102. const heartbeatTimer = setInterval(() => {
  103. if (!remote.destroyed) {
  104. debug('发送心跳包 时间:', new Date().toISOString());
  105. remote.write('OPTIONS /ping HTTP/1.1\r\nHost: ping\r\n\r\n');
  106. }
  107. }, heartbeatInterval);
  108. // 心跳检测
  109. const heartbeatCheck = setInterval(() => {
  110. const now = Date.now();
  111. if (now - lastDataTime > heartbeatTimeout) {
  112. debug('心跳检测超时 时间:', new Date().toISOString());
  113. remote.destroy(new Error('心跳检测失败'));
  114. clearInterval(heartbeatCheck);
  115. clearInterval(heartbeatTimer);
  116. }
  117. }, heartbeatInterval);
  118. // 接收数据时更新最后数据时间
  119. remote.on('data', (data) => {
  120. lastDataTime = Date.now();
  121. const dataStr = data.toString();
  122. // 任何响应都视为有效心跳
  123. if (dataStr.includes('HTTP/1.1')) {
  124. debug('收到服务器响应 时间:', new Date().toISOString());
  125. }
  126. const match = dataStr.match(/^(\w+) (\S+)/);
  127. if (match) {
  128. debug('收到请求: %s %s', match[1], match[2]);
  129. this.emit('request', {
  130. method: match[1],
  131. path: match[2],
  132. });
  133. }
  134. });
  135. const connLocal = () => {
  136. if (remote.destroyed) {
  137. debug('远程连接已销毁,无法建立本地连接 时间:', new Date().toISOString());
  138. clearInterval(heartbeatCheck);
  139. clearInterval(heartbeatTimer);
  140. this.emit('dead');
  141. return;
  142. }
  143. debug('正在连接本地服务 %s://%s:%d', localProtocol, localHost, localPort);
  144. remote.pause();
  145. if (allowInvalidCert) {
  146. debug('允许无效证书');
  147. }
  148. // 创建本地连接
  149. const local = opt.local_https
  150. ? tls.connect({
  151. host: localHost,
  152. port: localPort,
  153. ...(allowInvalidCert
  154. ? { rejectUnauthorized: false }
  155. : {
  156. cert: fs.readFileSync(opt.local_cert),
  157. key: fs.readFileSync(opt.local_key),
  158. ca: opt.local_ca ? [fs.readFileSync(opt.local_ca)] : undefined,
  159. }),
  160. })
  161. : net.connect({ host: localHost, port: localPort });
  162. // 监控本地连接状态
  163. local.on('connect', () => {
  164. debug('本地连接已建立 时间:', new Date().toISOString());
  165. });
  166. local.on('timeout', () => {
  167. debug('本地连接超时 时间:', new Date().toISOString());
  168. local.end();
  169. });
  170. local.on('end', () => {
  171. debug('本地连接结束 时间:', new Date().toISOString());
  172. });
  173. const remoteClose = () => {
  174. debug('远程连接关闭,清理本地连接 时间:', new Date().toISOString());
  175. this.emit('dead');
  176. local.end();
  177. };
  178. remote.once('close', remoteClose);
  179. local.once('error', err => {
  180. debug('本地连接错误: %s 时间:', new Date().toISOString(), err.message);
  181. local.end();
  182. remote.removeListener('close', remoteClose);
  183. if (err.code !== 'ECONNREFUSED' && err.code !== 'ECONNRESET') {
  184. debug('本地连接发生致命错误,关闭远程连接 时间:', new Date().toISOString());
  185. return remote.end();
  186. }
  187. debug('本地连接失败,1秒后重试 时间:', new Date().toISOString());
  188. setTimeout(connLocal, 1000);
  189. });
  190. local.once('connect', () => {
  191. debug('本地连接成功 时间:', new Date().toISOString());
  192. remote.resume();
  193. let stream = remote;
  194. // 如果指定了本地主机,使用 HeaderHostTransformer
  195. if (opt.local_host) {
  196. debug('转换 Host 头为 %s', opt.local_host);
  197. stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host }));
  198. }
  199. stream.pipe(local).pipe(remote);
  200. // 监控数据流
  201. stream.on('data', (chunk) => {
  202. lastDataTime = Date.now(); // 更新最后数据时间
  203. debug('收到数据 时间: %s 长度: %d', new Date().toISOString(), chunk.length);
  204. });
  205. // 清理函数
  206. const cleanup = () => {
  207. debug('清理资源 时间:', new Date().toISOString());
  208. clearInterval(heartbeatCheck);
  209. clearInterval(heartbeatTimer);
  210. if (stream) {
  211. stream.unpipe();
  212. }
  213. if (local) {
  214. local.unpipe();
  215. }
  216. };
  217. // 当连接关闭时清理资源
  218. remote.once('close', cleanup);
  219. local.once('close', cleanup);
  220. });
  221. };
  222. // 监控请求数据
  223. remote.on('data', data => {
  224. const match = data.toString().match(/^(\w+) (\S+)/);
  225. if (match) {
  226. debug('收到请求: %s %s', match[1], match[2]);
  227. this.emit('request', {
  228. method: match[1],
  229. path: match[2],
  230. });
  231. }
  232. });
  233. // 当远程连接建立时,认为隧道打开
  234. remote.once('connect', () => {
  235. debug('远程连接成功,开始建立本地连接 时间:', new Date().toISOString());
  236. this.emit('open', remote);
  237. connLocal();
  238. });
  239. }
  240. };