136 lines
4.7 KiB
JavaScript
136 lines
4.7 KiB
JavaScript
import debug from 'debug';
|
|
import { Agent } from 'http';
|
|
import net from 'net';
|
|
const DEFAULT_MAX_SOCKETS = 10;
|
|
export class TunnelAgent extends Agent {
|
|
started;
|
|
availableSockets;
|
|
clientId;
|
|
closed;
|
|
connectedSockets;
|
|
log;
|
|
maxTcpSockets;
|
|
server;
|
|
tunnelServer;
|
|
waitingCreateConn;
|
|
constructor(options = {}) {
|
|
super({ keepAlive: true, maxFreeSockets: 1 });
|
|
this.availableSockets = [];
|
|
this.waitingCreateConn = [];
|
|
this.clientId = options.clientId;
|
|
this.log = debug(`lt:TunnelAgent[${options.clientId}]`);
|
|
this.connectedSockets = 0;
|
|
this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS;
|
|
this.tunnelServer = options.tunnelServer;
|
|
// Only create a local server if no shared tunnel server is provided
|
|
if (!this.tunnelServer) {
|
|
this.server = net.createServer();
|
|
}
|
|
this.started = false;
|
|
this.closed = false;
|
|
}
|
|
createConnection(options, cb) {
|
|
if (this.closed) {
|
|
cb?.(new Error('closed'), null);
|
|
return null;
|
|
}
|
|
this.log('create connection');
|
|
const sock = this.availableSockets.shift();
|
|
if (!sock) {
|
|
if (cb)
|
|
this.waitingCreateConn.push(cb);
|
|
this.log('waiting connected: %s', this.connectedSockets);
|
|
this.log('waiting available: %s', this.availableSockets.length);
|
|
return undefined;
|
|
}
|
|
this.log('socket given');
|
|
cb?.(null, sock);
|
|
return sock;
|
|
}
|
|
destroy() {
|
|
if (this.tunnelServer && this.clientId) {
|
|
this.tunnelServer.unregisterHandler(this.clientId);
|
|
}
|
|
if (this.server) {
|
|
this.server.close();
|
|
}
|
|
super.destroy();
|
|
}
|
|
listen() {
|
|
if (this.started)
|
|
throw new Error('already started');
|
|
this.started = true;
|
|
// If using shared tunnel server, register handler and return port 0
|
|
// (the actual port is the tunnel server's port, handled externally)
|
|
if (this.tunnelServer && this.clientId) {
|
|
this.tunnelServer.registerHandler(this.clientId, (socket) => {
|
|
this._onConnection(socket);
|
|
});
|
|
this.log('registered with shared tunnel server');
|
|
// Return port 0 to indicate shared tunnel server mode
|
|
return Promise.resolve({ port: 0 });
|
|
}
|
|
// Legacy mode: create our own TCP server
|
|
if (!this.server) {
|
|
throw new Error('No server available');
|
|
}
|
|
this.server.on('close', this._onClose.bind(this));
|
|
this.server.on('connection', this._onConnection.bind(this));
|
|
this.server.on('error', (err) => {
|
|
if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT')
|
|
return;
|
|
console.error(err);
|
|
});
|
|
return new Promise((resolve) => {
|
|
this.server.listen(() => {
|
|
const addr = this.server.address();
|
|
this.log('tcp server listening on port: %d', addr.port);
|
|
resolve({ port: addr.port });
|
|
});
|
|
});
|
|
}
|
|
stats() {
|
|
return { connectedSockets: this.connectedSockets };
|
|
}
|
|
_onClose() {
|
|
this.closed = true;
|
|
this.log('closed tcp socket');
|
|
for (const conn of this.waitingCreateConn) {
|
|
conn(new Error('closed'), null);
|
|
}
|
|
this.waitingCreateConn = [];
|
|
this.emit('end');
|
|
}
|
|
_onConnection(socket) {
|
|
if (this.connectedSockets >= this.maxTcpSockets) {
|
|
this.log('no more sockets allowed');
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
socket.once('close', (hadError) => {
|
|
this.log('closed socket (error: %s)', hadError);
|
|
this.connectedSockets -= 1;
|
|
const idx = this.availableSockets.indexOf(socket);
|
|
if (idx >= 0)
|
|
this.availableSockets.splice(idx, 1);
|
|
this.log('connected sockets: %s', this.connectedSockets);
|
|
if (this.connectedSockets <= 0) {
|
|
this.log('all sockets disconnected');
|
|
this.emit('offline');
|
|
}
|
|
});
|
|
socket.once('error', () => socket.destroy());
|
|
if (this.connectedSockets === 0)
|
|
this.emit('online');
|
|
this.connectedSockets += 1;
|
|
this.log('new connection from: %s:%s', socket.remoteAddress, socket.remotePort);
|
|
const fn = this.waitingCreateConn.shift();
|
|
if (fn) {
|
|
this.log('giving socket to queued conn request');
|
|
setTimeout(() => fn(null, socket), 0);
|
|
return;
|
|
}
|
|
this.availableSockets.push(socket);
|
|
}
|
|
}
|
|
//# sourceMappingURL=TunnelAgent.js.map
|