@push.rocks/smartproxy 3.28.6 → 3.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.iptablesproxy.d.ts +79 -7
- package/dist_ts/classes.iptablesproxy.js +662 -67
- package/dist_ts/classes.networkproxy.d.ts +46 -1
- package/dist_ts/classes.networkproxy.js +347 -8
- package/dist_ts/classes.portproxy.d.ts +36 -0
- package/dist_ts/classes.portproxy.js +464 -365
- package/package.json +2 -2
- package/readme.md +80 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.iptablesproxy.ts +786 -68
- package/ts/classes.networkproxy.ts +417 -7
- package/ts/classes.portproxy.ts +652 -485
|
@@ -16,6 +16,10 @@ export interface INetworkProxyOptions {
|
|
|
16
16
|
allowHeaders?: string;
|
|
17
17
|
maxAge?: number;
|
|
18
18
|
};
|
|
19
|
+
|
|
20
|
+
// New settings for PortProxy integration
|
|
21
|
+
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
|
22
|
+
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
|
@@ -42,13 +46,25 @@ export class NetworkProxy {
|
|
|
42
46
|
public requestsServed: number = 0;
|
|
43
47
|
public failedRequests: number = 0;
|
|
44
48
|
|
|
49
|
+
// New tracking for PortProxy integration
|
|
50
|
+
private portProxyConnections: number = 0;
|
|
51
|
+
private tlsTerminatedConnections: number = 0;
|
|
52
|
+
|
|
45
53
|
// Timers and intervals
|
|
46
54
|
private heartbeatInterval: NodeJS.Timeout;
|
|
47
55
|
private metricsInterval: NodeJS.Timeout;
|
|
56
|
+
private connectionPoolCleanupInterval: NodeJS.Timeout;
|
|
48
57
|
|
|
49
58
|
// Certificates
|
|
50
59
|
private defaultCertificates: { key: string; cert: string };
|
|
51
60
|
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
|
61
|
+
|
|
62
|
+
// New connection pool for backend connections
|
|
63
|
+
private connectionPool: Map<string, Array<{
|
|
64
|
+
socket: plugins.net.Socket;
|
|
65
|
+
lastUsed: number;
|
|
66
|
+
isIdle: boolean;
|
|
67
|
+
}>> = new Map();
|
|
52
68
|
|
|
53
69
|
/**
|
|
54
70
|
* Creates a new NetworkProxy instance
|
|
@@ -66,7 +82,10 @@ export class NetworkProxy {
|
|
|
66
82
|
allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
67
83
|
allowHeaders: 'Content-Type, Authorization',
|
|
68
84
|
maxAge: 86400
|
|
69
|
-
}
|
|
85
|
+
},
|
|
86
|
+
// New defaults for PortProxy integration
|
|
87
|
+
connectionPoolSize: optionsArg.connectionPoolSize || 50,
|
|
88
|
+
portProxyIntegration: optionsArg.portProxyIntegration || false
|
|
70
89
|
};
|
|
71
90
|
|
|
72
91
|
this.loadDefaultCertificates();
|
|
@@ -104,6 +123,213 @@ export class NetworkProxy {
|
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Returns the port number this NetworkProxy is listening on
|
|
128
|
+
* Useful for PortProxy to determine where to forward connections
|
|
129
|
+
*/
|
|
130
|
+
public getListeningPort(): number {
|
|
131
|
+
return this.options.port;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Updates the server capacity settings
|
|
136
|
+
* @param maxConnections Maximum number of simultaneous connections
|
|
137
|
+
* @param keepAliveTimeout Keep-alive timeout in milliseconds
|
|
138
|
+
* @param connectionPoolSize Size of the connection pool per backend
|
|
139
|
+
*/
|
|
140
|
+
public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
|
|
141
|
+
if (maxConnections !== undefined) {
|
|
142
|
+
this.options.maxConnections = maxConnections;
|
|
143
|
+
this.log('info', `Updated max connections to ${maxConnections}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (keepAliveTimeout !== undefined) {
|
|
147
|
+
this.options.keepAliveTimeout = keepAliveTimeout;
|
|
148
|
+
|
|
149
|
+
if (this.httpsServer) {
|
|
150
|
+
this.httpsServer.keepAliveTimeout = keepAliveTimeout;
|
|
151
|
+
this.log('info', `Updated keep-alive timeout to ${keepAliveTimeout}ms`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (connectionPoolSize !== undefined) {
|
|
156
|
+
this.options.connectionPoolSize = connectionPoolSize;
|
|
157
|
+
this.log('info', `Updated connection pool size to ${connectionPoolSize}`);
|
|
158
|
+
|
|
159
|
+
// Cleanup excess connections in the pool if the size was reduced
|
|
160
|
+
this.cleanupConnectionPool();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns current server metrics
|
|
166
|
+
* Useful for PortProxy to determine which NetworkProxy to use for load balancing
|
|
167
|
+
*/
|
|
168
|
+
public getMetrics(): any {
|
|
169
|
+
return {
|
|
170
|
+
activeConnections: this.connectedClients,
|
|
171
|
+
totalRequests: this.requestsServed,
|
|
172
|
+
failedRequests: this.failedRequests,
|
|
173
|
+
portProxyConnections: this.portProxyConnections,
|
|
174
|
+
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
175
|
+
connectionPoolSize: Array.from(this.connectionPool.entries()).reduce((acc, [host, connections]) => {
|
|
176
|
+
acc[host] = connections.length;
|
|
177
|
+
return acc;
|
|
178
|
+
}, {} as Record<string, number>),
|
|
179
|
+
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
|
180
|
+
memoryUsage: process.memoryUsage(),
|
|
181
|
+
activeWebSockets: this.wsServer?.clients.size || 0
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cleanup the connection pool by removing idle connections
|
|
187
|
+
* or reducing pool size if it exceeds the configured maximum
|
|
188
|
+
*/
|
|
189
|
+
private cleanupConnectionPool(): void {
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
|
|
192
|
+
|
|
193
|
+
for (const [host, connections] of this.connectionPool.entries()) {
|
|
194
|
+
// Sort by last used time (oldest first)
|
|
195
|
+
connections.sort((a, b) => a.lastUsed - b.lastUsed);
|
|
196
|
+
|
|
197
|
+
// Remove idle connections older than the idle timeout
|
|
198
|
+
let removed = 0;
|
|
199
|
+
while (connections.length > 0) {
|
|
200
|
+
const connection = connections[0];
|
|
201
|
+
|
|
202
|
+
// Remove if idle and exceeds timeout, or if pool is too large
|
|
203
|
+
if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
|
|
204
|
+
connections.length > this.options.connectionPoolSize!) {
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
if (!connection.socket.destroyed) {
|
|
208
|
+
connection.socket.end();
|
|
209
|
+
connection.socket.destroy();
|
|
210
|
+
}
|
|
211
|
+
} catch (err) {
|
|
212
|
+
this.log('error', `Error destroying pooled connection to ${host}`, err);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
connections.shift(); // Remove from pool
|
|
216
|
+
removed++;
|
|
217
|
+
} else {
|
|
218
|
+
break; // Stop removing if we've reached active or recent connections
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (removed > 0) {
|
|
223
|
+
this.log('debug', `Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Update the pool with the remaining connections
|
|
227
|
+
if (connections.length === 0) {
|
|
228
|
+
this.connectionPool.delete(host);
|
|
229
|
+
} else {
|
|
230
|
+
this.connectionPool.set(host, connections);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get a connection from the pool or create a new one
|
|
237
|
+
*/
|
|
238
|
+
private getConnectionFromPool(host: string, port: number): Promise<plugins.net.Socket> {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const poolKey = `${host}:${port}`;
|
|
241
|
+
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
242
|
+
|
|
243
|
+
// Look for an idle connection
|
|
244
|
+
const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
|
|
245
|
+
|
|
246
|
+
if (idleConnectionIndex >= 0) {
|
|
247
|
+
// Get existing connection from pool
|
|
248
|
+
const connection = connectionList[idleConnectionIndex];
|
|
249
|
+
connection.isIdle = false;
|
|
250
|
+
connection.lastUsed = Date.now();
|
|
251
|
+
this.log('debug', `Reusing connection from pool for ${poolKey}`);
|
|
252
|
+
|
|
253
|
+
// Update the pool
|
|
254
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
255
|
+
|
|
256
|
+
resolve(connection.socket);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// No idle connection available, create a new one if pool isn't full
|
|
261
|
+
if (connectionList.length < this.options.connectionPoolSize!) {
|
|
262
|
+
this.log('debug', `Creating new connection to ${host}:${port}`);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const socket = plugins.net.connect({
|
|
266
|
+
host,
|
|
267
|
+
port,
|
|
268
|
+
keepAlive: true,
|
|
269
|
+
keepAliveInitialDelay: 30000 // 30 seconds
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
socket.once('connect', () => {
|
|
273
|
+
// Add to connection pool
|
|
274
|
+
const connection = {
|
|
275
|
+
socket,
|
|
276
|
+
lastUsed: Date.now(),
|
|
277
|
+
isIdle: false
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
connectionList.push(connection);
|
|
281
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
282
|
+
|
|
283
|
+
// Setup cleanup when the connection is closed
|
|
284
|
+
socket.once('close', () => {
|
|
285
|
+
const idx = connectionList.findIndex(c => c.socket === socket);
|
|
286
|
+
if (idx >= 0) {
|
|
287
|
+
connectionList.splice(idx, 1);
|
|
288
|
+
this.connectionPool.set(poolKey, connectionList);
|
|
289
|
+
this.log('debug', `Removed closed connection from pool for ${poolKey}`);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
resolve(socket);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
socket.once('error', (err) => {
|
|
297
|
+
this.log('error', `Error creating connection to ${host}:${port}`, err);
|
|
298
|
+
reject(err);
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
this.log('error', `Failed to create connection to ${host}:${port}`, err);
|
|
302
|
+
reject(err);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
// Pool is full, wait for an idle connection or reject
|
|
306
|
+
this.log('warn', `Connection pool for ${poolKey} is full (${connectionList.length})`);
|
|
307
|
+
reject(new Error(`Connection pool for ${poolKey} is full`));
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Return a connection to the pool for reuse
|
|
314
|
+
*/
|
|
315
|
+
private returnConnectionToPool(socket: plugins.net.Socket, host: string, port: number): void {
|
|
316
|
+
const poolKey = `${host}:${port}`;
|
|
317
|
+
const connectionList = this.connectionPool.get(poolKey) || [];
|
|
318
|
+
|
|
319
|
+
// Find this connection in the pool
|
|
320
|
+
const connectionIndex = connectionList.findIndex(c => c.socket === socket);
|
|
321
|
+
|
|
322
|
+
if (connectionIndex >= 0) {
|
|
323
|
+
// Mark as idle and update last used time
|
|
324
|
+
connectionList[connectionIndex].isIdle = true;
|
|
325
|
+
connectionList[connectionIndex].lastUsed = Date.now();
|
|
326
|
+
|
|
327
|
+
this.log('debug', `Returned connection to pool for ${poolKey}`);
|
|
328
|
+
} else {
|
|
329
|
+
this.log('warn', `Attempted to return unknown connection to pool for ${poolKey}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
107
333
|
/**
|
|
108
334
|
* Starts the proxy server
|
|
109
335
|
*/
|
|
@@ -131,6 +357,9 @@ export class NetworkProxy {
|
|
|
131
357
|
|
|
132
358
|
// Start metrics collection
|
|
133
359
|
this.setupMetricsCollection();
|
|
360
|
+
|
|
361
|
+
// Setup connection pool cleanup interval
|
|
362
|
+
this.setupConnectionPoolCleanup();
|
|
134
363
|
|
|
135
364
|
// Start the server
|
|
136
365
|
return new Promise((resolve) => {
|
|
@@ -156,13 +385,31 @@ export class NetworkProxy {
|
|
|
156
385
|
// Add connection to tracking
|
|
157
386
|
this.socketMap.add(connection);
|
|
158
387
|
this.connectedClients = this.socketMap.getArray().length;
|
|
159
|
-
|
|
388
|
+
|
|
389
|
+
// Check for connection from PortProxy by inspecting the source port
|
|
390
|
+
// This is a heuristic - in a production environment you might use a more robust method
|
|
391
|
+
const localPort = connection.localPort;
|
|
392
|
+
const remotePort = connection.remotePort;
|
|
393
|
+
|
|
394
|
+
// If this connection is from a PortProxy (usually indicated by it coming from localhost)
|
|
395
|
+
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
396
|
+
this.portProxyConnections++;
|
|
397
|
+
this.log('debug', `New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
|
|
398
|
+
} else {
|
|
399
|
+
this.log('debug', `New direct connection (local: ${localPort}, remote: ${remotePort})`);
|
|
400
|
+
}
|
|
160
401
|
|
|
161
402
|
// Setup connection cleanup handlers
|
|
162
403
|
const cleanupConnection = () => {
|
|
163
404
|
if (this.socketMap.checkForObject(connection)) {
|
|
164
405
|
this.socketMap.remove(connection);
|
|
165
406
|
this.connectedClients = this.socketMap.getArray().length;
|
|
407
|
+
|
|
408
|
+
// If this was a PortProxy connection, decrement the counter
|
|
409
|
+
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
|
410
|
+
this.portProxyConnections--;
|
|
411
|
+
}
|
|
412
|
+
|
|
166
413
|
this.log('debug', `Connection closed. ${this.connectedClients} connections remaining`);
|
|
167
414
|
}
|
|
168
415
|
};
|
|
@@ -178,6 +425,12 @@ export class NetworkProxy {
|
|
|
178
425
|
cleanupConnection();
|
|
179
426
|
});
|
|
180
427
|
});
|
|
428
|
+
|
|
429
|
+
// Track TLS handshake completions
|
|
430
|
+
this.httpsServer.on('secureConnection', (tlsSocket) => {
|
|
431
|
+
this.tlsTerminatedConnections++;
|
|
432
|
+
this.log('debug', 'TLS handshake completed, connection secured');
|
|
433
|
+
});
|
|
181
434
|
}
|
|
182
435
|
|
|
183
436
|
/**
|
|
@@ -228,14 +481,35 @@ export class NetworkProxy {
|
|
|
228
481
|
activeConnections: this.connectedClients,
|
|
229
482
|
totalRequests: this.requestsServed,
|
|
230
483
|
failedRequests: this.failedRequests,
|
|
484
|
+
portProxyConnections: this.portProxyConnections,
|
|
485
|
+
tlsTerminatedConnections: this.tlsTerminatedConnections,
|
|
231
486
|
activeWebSockets: this.wsServer?.clients.size || 0,
|
|
232
487
|
memoryUsage: process.memoryUsage(),
|
|
233
|
-
activeContexts: Array.from(this.activeContexts)
|
|
488
|
+
activeContexts: Array.from(this.activeContexts),
|
|
489
|
+
connectionPool: Object.fromEntries(
|
|
490
|
+
Array.from(this.connectionPool.entries()).map(([host, connections]) => [
|
|
491
|
+
host,
|
|
492
|
+
{
|
|
493
|
+
total: connections.length,
|
|
494
|
+
idle: connections.filter(c => c.isIdle).length
|
|
495
|
+
}
|
|
496
|
+
])
|
|
497
|
+
)
|
|
234
498
|
};
|
|
235
499
|
|
|
236
500
|
this.log('debug', 'Proxy metrics', metrics);
|
|
237
501
|
}, 60000); // Log metrics every minute
|
|
238
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Sets up connection pool cleanup
|
|
506
|
+
*/
|
|
507
|
+
private setupConnectionPoolCleanup(): void {
|
|
508
|
+
// Clean up idle connections every minute
|
|
509
|
+
this.connectionPoolCleanupInterval = setInterval(() => {
|
|
510
|
+
this.cleanupConnectionPool();
|
|
511
|
+
}, 60000); // 1 minute
|
|
512
|
+
}
|
|
239
513
|
|
|
240
514
|
/**
|
|
241
515
|
* Handles an incoming WebSocket connection
|
|
@@ -410,12 +684,27 @@ export class NetworkProxy {
|
|
|
410
684
|
}
|
|
411
685
|
}
|
|
412
686
|
|
|
687
|
+
// Determine if we should use connection pooling
|
|
688
|
+
const useConnectionPool = this.options.portProxyIntegration &&
|
|
689
|
+
originRequest.socket.remoteAddress?.includes('127.0.0.1');
|
|
690
|
+
|
|
413
691
|
// Construct destination URL
|
|
414
692
|
const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
|
|
415
|
-
this.log('debug', `[${reqId}] Proxying to ${destinationUrl}`);
|
|
416
693
|
|
|
417
|
-
|
|
418
|
-
|
|
694
|
+
if (useConnectionPool) {
|
|
695
|
+
this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
|
|
696
|
+
await this.forwardRequestUsingConnectionPool(
|
|
697
|
+
reqId,
|
|
698
|
+
originRequest,
|
|
699
|
+
originResponse,
|
|
700
|
+
destinationConfig.destinationIp,
|
|
701
|
+
destinationConfig.destinationPort,
|
|
702
|
+
originRequest.url
|
|
703
|
+
);
|
|
704
|
+
} else {
|
|
705
|
+
this.log('debug', `[${reqId}] Proxying to ${destinationUrl}`);
|
|
706
|
+
await this.forwardRequest(reqId, originRequest, originResponse, destinationUrl);
|
|
707
|
+
}
|
|
419
708
|
|
|
420
709
|
const processingTime = Date.now() - startTime;
|
|
421
710
|
this.log('debug', `[${reqId}] Request completed in ${processingTime}ms`);
|
|
@@ -488,7 +777,105 @@ export class NetworkProxy {
|
|
|
488
777
|
}
|
|
489
778
|
|
|
490
779
|
/**
|
|
491
|
-
* Forwards a request to the destination
|
|
780
|
+
* Forwards a request to the destination using connection pool
|
|
781
|
+
* for optimized connection reuse from PortProxy
|
|
782
|
+
*/
|
|
783
|
+
private async forwardRequestUsingConnectionPool(
|
|
784
|
+
reqId: string,
|
|
785
|
+
originRequest: plugins.http.IncomingMessage,
|
|
786
|
+
originResponse: plugins.http.ServerResponse,
|
|
787
|
+
host: string,
|
|
788
|
+
port: number,
|
|
789
|
+
path: string
|
|
790
|
+
): Promise<void> {
|
|
791
|
+
try {
|
|
792
|
+
// Try to get a connection from the pool
|
|
793
|
+
const socket = await this.getConnectionFromPool(host, port);
|
|
794
|
+
|
|
795
|
+
// Create an HTTP client request using the pooled socket
|
|
796
|
+
const reqOptions = {
|
|
797
|
+
createConnection: () => socket,
|
|
798
|
+
host,
|
|
799
|
+
port,
|
|
800
|
+
path,
|
|
801
|
+
method: originRequest.method,
|
|
802
|
+
headers: this.prepareForwardHeaders(originRequest),
|
|
803
|
+
timeout: 30000 // 30 second timeout
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
const proxyReq = plugins.http.request(reqOptions);
|
|
807
|
+
|
|
808
|
+
// Handle timeouts
|
|
809
|
+
proxyReq.on('timeout', () => {
|
|
810
|
+
this.log('warn', `[${reqId}] Request to ${host}:${port}${path} timed out`);
|
|
811
|
+
proxyReq.destroy();
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// Handle errors
|
|
815
|
+
proxyReq.on('error', (err) => {
|
|
816
|
+
this.log('error', `[${reqId}] Error in proxy request to ${host}:${port}${path}`, err);
|
|
817
|
+
|
|
818
|
+
// Check if the client response is still writable
|
|
819
|
+
if (!originResponse.writableEnded) {
|
|
820
|
+
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Error communicating with upstream server');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Don't return the socket to the pool on error
|
|
824
|
+
try {
|
|
825
|
+
if (!socket.destroyed) {
|
|
826
|
+
socket.destroy();
|
|
827
|
+
}
|
|
828
|
+
} catch (socketErr) {
|
|
829
|
+
this.log('error', `[${reqId}] Error destroying socket after request error`, socketErr);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Forward request body
|
|
834
|
+
originRequest.pipe(proxyReq);
|
|
835
|
+
|
|
836
|
+
// Handle response
|
|
837
|
+
proxyReq.on('response', (proxyRes) => {
|
|
838
|
+
// Copy status and headers
|
|
839
|
+
originResponse.statusCode = proxyRes.statusCode;
|
|
840
|
+
|
|
841
|
+
for (const [name, value] of Object.entries(proxyRes.headers)) {
|
|
842
|
+
if (value !== undefined) {
|
|
843
|
+
originResponse.setHeader(name, value);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Forward the response body
|
|
848
|
+
proxyRes.pipe(originResponse);
|
|
849
|
+
|
|
850
|
+
// Return connection to pool when the response completes
|
|
851
|
+
proxyRes.on('end', () => {
|
|
852
|
+
if (!socket.destroyed) {
|
|
853
|
+
this.returnConnectionToPool(socket, host, port);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
proxyRes.on('error', (err) => {
|
|
858
|
+
this.log('error', `[${reqId}] Error in proxy response from ${host}:${port}${path}`, err);
|
|
859
|
+
|
|
860
|
+
// Don't return the socket to the pool on error
|
|
861
|
+
try {
|
|
862
|
+
if (!socket.destroyed) {
|
|
863
|
+
socket.destroy();
|
|
864
|
+
}
|
|
865
|
+
} catch (socketErr) {
|
|
866
|
+
this.log('error', `[${reqId}] Error destroying socket after response error`, socketErr);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
this.log('error', `[${reqId}] Error setting up pooled connection to ${host}:${port}`, error);
|
|
872
|
+
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Forwards a request to the destination (standard method)
|
|
492
879
|
*/
|
|
493
880
|
private async forwardRequest(
|
|
494
881
|
reqId: string,
|
|
@@ -532,6 +919,11 @@ export class NetworkProxy {
|
|
|
532
919
|
// Add proxy-specific headers
|
|
533
920
|
safeHeaders['X-Proxy-Id'] = `NetworkProxy-${this.options.port}`;
|
|
534
921
|
|
|
922
|
+
// If this is coming from PortProxy, add a header to indicate that
|
|
923
|
+
if (this.options.portProxyIntegration && req.socket.remoteAddress?.includes('127.0.0.1')) {
|
|
924
|
+
safeHeaders['X-PortProxy-Forwarded'] = 'true';
|
|
925
|
+
}
|
|
926
|
+
|
|
535
927
|
// Remove sensitive headers we don't want to forward
|
|
536
928
|
const sensitiveHeaders = ['connection', 'upgrade', 'http2-settings'];
|
|
537
929
|
for (const header of sensitiveHeaders) {
|
|
@@ -778,6 +1170,10 @@ export class NetworkProxy {
|
|
|
778
1170
|
clearInterval(this.metricsInterval);
|
|
779
1171
|
}
|
|
780
1172
|
|
|
1173
|
+
if (this.connectionPoolCleanupInterval) {
|
|
1174
|
+
clearInterval(this.connectionPoolCleanupInterval);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
781
1177
|
// Close WebSocket server if exists
|
|
782
1178
|
if (this.wsServer) {
|
|
783
1179
|
for (const client of this.wsServer.clients) {
|
|
@@ -798,6 +1194,20 @@ export class NetworkProxy {
|
|
|
798
1194
|
}
|
|
799
1195
|
}
|
|
800
1196
|
|
|
1197
|
+
// Close all connection pool connections
|
|
1198
|
+
for (const [host, connections] of this.connectionPool.entries()) {
|
|
1199
|
+
for (const connection of connections) {
|
|
1200
|
+
try {
|
|
1201
|
+
if (!connection.socket.destroyed) {
|
|
1202
|
+
connection.socket.destroy();
|
|
1203
|
+
}
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
this.log('error', `Error destroying pooled connection to ${host}`, error);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
this.connectionPool.clear();
|
|
1210
|
+
|
|
801
1211
|
// Close the HTTPS server
|
|
802
1212
|
return new Promise((resolve) => {
|
|
803
1213
|
this.httpsServer.close(() => {
|