"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = require("events"); const debug_1 = __importDefault(require("debug")); const fs_1 = __importDefault(require("fs")); const net_1 = __importDefault(require("net")); const tls_1 = __importDefault(require("tls")); const HeaderHostTransformer_1 = __importDefault(require("./HeaderHostTransformer")); const debugLog = (0, debug_1.default)('localtunnel:client'); class TunnelCluster extends events_1.EventEmitter { constructor(opts = {}) { super(); this.isRegistered = false; this.opts = opts; this.remoteHost = opts.remote_ip || opts.remote_host || 'localhost'; this.remotePort = opts.remote_port || 7002; this.clientId = opts.name || ''; } open() { const opt = this.opts; const remoteHostOrIp = opt.remote_ip || opt.remote_host; const remotePort = opt.remote_port; const localHost = opt.local_host || 'localhost'; const localPort = opt.local_port; if (!localPort || typeof localPort !== 'number') { this.emit('error', new Error('Invalid local port configuration')); return; } const localProtocol = opt.local_https ? 'https' : 'http'; if (!remoteHostOrIp || !remotePort) { this.emit('error', new Error('Missing remote host/port configuration')); return; } debugLog('establishing tunnel %s://%s:%s <> %s:%s', localProtocol, localHost, localPort, remoteHostOrIp, remotePort); const remote = net_1.default.connect({ host: remoteHostOrIp, port: remotePort }); remote.setKeepAlive(true); const registrationTimeout = setTimeout(() => { if (!this.isRegistered) { debugLog('registration timeout'); remote.destroy(new Error('Registration timeout')); } }, 5000); remote.once('connect', () => { debugLog('发送身份识别信息'); remote.write(JSON.stringify({ clientId: this.clientId, type: 'register' }) + '\n'); let buffer = ''; const messageHandler = (data) => { buffer += data.toString(); if (buffer.includes('\n')) { try { const response = JSON.parse(buffer); if (response.type === 'registered' && response.success) { clearTimeout(registrationTimeout); this.isRegistered = true; remote.removeListener('data', messageHandler); this.emit('open', remote); connLocal(); } else if (response.type === 'registered' && !response.success) { remote.destroy(new Error(response.message || 'Registration failed')); } } catch (err) { debugLog('解析注册响应失败:', err); remote.destroy(); } buffer = ''; } }; remote.on('data', messageHandler); }); remote.on('error', (err) => { clearTimeout(registrationTimeout); debugLog('remote connection error:', err.message); if (err.code === 'ECONNREFUSED') { this.emit('error', new Error(`Connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)`)); } remote.end(); }); const connLocal = () => { if (remote.destroyed) { debugLog('remote destroyed'); this.emit('dead'); return; } debugLog('connecting locally to %s://%s:%d', localProtocol, localHost, localPort); remote.pause(); const local = this.createLocalConnection(localPort); local.once('error', (err) => { debugLog('local error %s', err.message); local.end(); if (err.code !== 'ECONNREFUSED' && err.code !== 'ECONNRESET') { return remote.end(); } setTimeout(connLocal, 1000); }); local.once('connect', () => { debugLog('connected locally'); remote.resume(); this.setupPipes(remote, local); }); }; } createLocalConnection(localPort) { const { local_host, local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts; const localConnectOpts = { host: local_host || 'localhost', port: localPort }; if (!local_https) { return net_1.default.connect(localConnectOpts); } const tlsOpts = allow_invalid_cert ? { rejectUnauthorized: false } : { cert: local_cert ? fs_1.default.readFileSync(local_cert) : undefined, key: local_key ? fs_1.default.readFileSync(local_key) : undefined, ca: local_ca ? [fs_1.default.readFileSync(local_ca)] : undefined, }; return tls_1.default.connect({ ...localConnectOpts, ...tlsOpts }); } setupPipes(remote, local) { let stream = remote; if (this.opts.local_host) { debugLog('transform Host header to %s', this.opts.local_host); const transformer = new HeaderHostTransformer_1.default({ host: this.opts.local_host }); stream = remote.pipe(transformer); } stream.pipe(local).pipe(remote); local.once('close', (hadError) => { debugLog('local connection closed [%s]', hadError); if (!remote.destroyed) { remote.end(); } }); remote.once('close', () => { debugLog('remote close'); this.emit('dead'); if (!local.destroyed) { local.end(); } }); } } exports.default = TunnelCluster;