-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathTunnelAgent.js
More file actions
187 lines (156 loc) · 5.93 KB
/
Copy pathTunnelAgent.js
File metadata and controls
187 lines (156 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { Agent } from 'http';
import net from 'net';
import assert from 'assert';
import log from 'book';
import Debug from 'debug';
import { getPort } from 'portfinder';
const DEFAULT_MAX_SOCKETS = 10;
const DEFAULT_MIN_PORT_RANGE = 1024;
const DEFAULT_MAX_PORT_RANGE = 65535;
// Implements an http.Agent interface to a pool of tunnel sockets
// A tunnel socket is a connection _from_ a client that will
// service http requests. This agent is usable wherever one can use an http.Agent
class TunnelAgent extends Agent {
constructor(options = {}) {
super({
keepAlive: true,
// only allow keepalive to hold on to one socket
// this prevents it from holding on to all the sockets so they can be used for upgrades
maxFreeSockets: 1,
});
// sockets we can hand out via createConnection
this.availableSockets = [];
// when a createConnection cannot return a socket, it goes into a queue
// once a socket is available it is handed out to the next callback
this.waitingCreateConn = [];
this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`);
// track maximum allowed sockets
this.connectedSockets = 0;
this.maxTcpSockets = options.maxTcpSockets || DEFAULT_MAX_SOCKETS;
this.client_min_port_range = options.client_min_port_range || DEFAULT_MIN_PORT_RANGE;
this.client_max_port_range = options.client_max_port_range || DEFAULT_MAX_PORT_RANGE;
// new tcp server to service requests for this client
this.server = net.createServer();
// flag to avoid double starts
this.started = false;
this.closed = false;
}
stats() {
return {
connectedSockets: this.connectedSockets,
};
}
listen() {
const server = this.server;
if (this.started) {
throw new Error('already started');
}
this.started = true;
server.on('close', this._onClose.bind(this));
server.on('connection', this._onConnection.bind(this));
server.on('error', (err) => {
// These errors happen from killed connections, we don't worry about them
if (err.code == 'ECONNRESET' || err.code == 'ETIMEDOUT') {
return;
}
log.error(err);
});
return new Promise((resolve) => {
getPort({
port: this.client_min_port_range, // minimum port
stopPort: this.client_max_port_range // maximum port
}, (err,port) => {
server.listen(port,() => {
const port = server.address().port;
this.debug('tcp server listening on port: %d', port);
resolve({
// port for lt client tcp connections
port: port,
});
});
});
});
}
_onClose() {
this.closed = true;
this.debug('closed tcp socket');
// flush any waiting connections
for (const conn of this.waitingCreateConn) {
conn(new Error('closed'), null);
}
this.waitingCreateConn = [];
this.emit('end');
}
// new socket connection from client for tunneling requests to client
_onConnection(socket) {
// no more socket connections allowed
if (this.connectedSockets >= this.maxTcpSockets) {
this.debug('no more sockets allowed');
socket.destroy();
return false;
}
socket.once('close', (hadError) => {
this.debug('closed socket (error: %s)', hadError);
this.connectedSockets -= 1;
// remove the socket from available list
const idx = this.availableSockets.indexOf(socket);
if (idx >= 0) {
this.availableSockets.splice(idx, 1);
}
this.debug('connected sockets: %s', this.connectedSockets);
if (this.connectedSockets <= 0) {
this.debug('all sockets disconnected');
this.emit('offline');
}
});
// close will be emitted after this
socket.once('error', (err) => {
// we do not log these errors, sessions can drop from clients for many reasons
// these are not actionable errors for our server
socket.destroy();
});
if (this.connectedSockets === 0) {
this.emit('online');
}
this.connectedSockets += 1;
this.debug('new connection from: %s:%s', socket.address().address, socket.address().port);
// if there are queued callbacks, give this socket now and don't queue into available
const fn = this.waitingCreateConn.shift();
if (fn) {
this.debug('giving socket to queued conn request');
setTimeout(() => {
fn(null, socket);
}, 0);
return;
}
// make socket available for those waiting on sockets
this.availableSockets.push(socket);
}
// fetch a socket from the available socket pool for the agent
// if no socket is available, queue
// cb(err, socket)
createConnection(options, cb) {
if (this.closed) {
cb(new Error('closed'));
return;
}
this.debug('create connection');
// socket is a tcp connection back to the user hosting the site
const sock = this.availableSockets.shift();
// no available sockets
// wait until we have one
if (!sock) {
this.waitingCreateConn.push(cb);
this.debug('waiting connected: %s', this.connectedSockets);
this.debug('waiting available: %s', this.availableSockets.length);
return;
}
this.debug('socket given');
cb(null, sock);
}
destroy() {
this.server.close();
super.destroy();
}
}
export default TunnelAgent;