@push.rocks/smartproxy 3.23.0 → 3.24.0
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.networkproxy.d.ts +90 -8
- package/dist_ts/classes.networkproxy.js +605 -221
- package/dist_ts/classes.portproxy.d.ts +5 -0
- package/dist_ts/classes.portproxy.js +251 -80
- package/package.json +8 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.networkproxy.ts +743 -268
- package/ts/classes.portproxy.ts +285 -84
|
@@ -6,28 +6,76 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
|
|
7
7
|
export interface INetworkProxyOptions {
|
|
8
8
|
port: number;
|
|
9
|
+
maxConnections?: number;
|
|
10
|
+
keepAliveTimeout?: number;
|
|
11
|
+
headersTimeout?: number;
|
|
12
|
+
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
|
13
|
+
cors?: {
|
|
14
|
+
allowOrigin?: string;
|
|
15
|
+
allowMethods?: string;
|
|
16
|
+
allowHeaders?: string;
|
|
17
|
+
maxAge?: number;
|
|
18
|
+
};
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
|
12
22
|
lastPong: number;
|
|
23
|
+
isAlive: boolean;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export class NetworkProxy {
|
|
27
|
+
// Configuration
|
|
16
28
|
public options: INetworkProxyOptions;
|
|
17
29
|
public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
|
30
|
+
public defaultHeaders: { [key: string]: string } = {};
|
|
31
|
+
|
|
32
|
+
// Server instances
|
|
18
33
|
public httpsServer: plugins.https.Server;
|
|
34
|
+
public wsServer: plugins.ws.WebSocketServer;
|
|
35
|
+
|
|
36
|
+
// State tracking
|
|
19
37
|
public router = new ProxyRouter();
|
|
20
38
|
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
|
21
|
-
public
|
|
22
|
-
public
|
|
39
|
+
public activeContexts: Set<string> = new Set();
|
|
40
|
+
public connectedClients: number = 0;
|
|
41
|
+
public startTime: number = 0;
|
|
42
|
+
public requestsServed: number = 0;
|
|
43
|
+
public failedRequests: number = 0;
|
|
44
|
+
|
|
45
|
+
// Timers and intervals
|
|
46
|
+
private heartbeatInterval: NodeJS.Timeout;
|
|
47
|
+
private metricsInterval: NodeJS.Timeout;
|
|
48
|
+
|
|
49
|
+
// Certificates
|
|
23
50
|
private defaultCertificates: { key: string; cert: string };
|
|
51
|
+
private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new NetworkProxy instance
|
|
55
|
+
*/
|
|
29
56
|
constructor(optionsArg: INetworkProxyOptions) {
|
|
30
|
-
|
|
57
|
+
// Set default options
|
|
58
|
+
this.options = {
|
|
59
|
+
port: optionsArg.port,
|
|
60
|
+
maxConnections: optionsArg.maxConnections || 10000,
|
|
61
|
+
keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes
|
|
62
|
+
headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
|
|
63
|
+
logLevel: optionsArg.logLevel || 'info',
|
|
64
|
+
cors: optionsArg.cors || {
|
|
65
|
+
allowOrigin: '*',
|
|
66
|
+
allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
67
|
+
allowHeaders: 'Content-Type, Authorization',
|
|
68
|
+
maxAge: 86400
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.loadDefaultCertificates();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Loads default certificates from the filesystem
|
|
77
|
+
*/
|
|
78
|
+
private loadDefaultCertificates(): void {
|
|
31
79
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
80
|
const certPath = path.join(__dirname, '..', 'assets', 'certs');
|
|
33
81
|
|
|
@@ -36,334 +84,761 @@ export class NetworkProxy {
|
|
|
36
84
|
key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
|
|
37
85
|
cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
|
|
38
86
|
};
|
|
87
|
+
this.log('info', 'Default certificates loaded successfully');
|
|
39
88
|
} catch (error) {
|
|
40
|
-
|
|
41
|
-
|
|
89
|
+
this.log('error', 'Error loading default certificates', error);
|
|
90
|
+
|
|
91
|
+
// Generate self-signed fallback certificates
|
|
92
|
+
try {
|
|
93
|
+
// This is a placeholder for actual certificate generation code
|
|
94
|
+
// In a real implementation, you would use a library like selfsigned to generate certs
|
|
95
|
+
this.defaultCertificates = {
|
|
96
|
+
key: "FALLBACK_KEY_CONTENT",
|
|
97
|
+
cert: "FALLBACK_CERT_CONTENT"
|
|
98
|
+
};
|
|
99
|
+
this.log('warn', 'Using fallback self-signed certificates');
|
|
100
|
+
} catch (fallbackError) {
|
|
101
|
+
this.log('error', 'Failed to generate fallback certificates', fallbackError);
|
|
102
|
+
throw new Error('Could not load or generate SSL certificates');
|
|
103
|
+
}
|
|
42
104
|
}
|
|
43
105
|
}
|
|
44
106
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Starts the proxy server
|
|
109
|
+
*/
|
|
110
|
+
public async start(): Promise<void> {
|
|
111
|
+
this.startTime = Date.now();
|
|
112
|
+
|
|
113
|
+
// Create the HTTPS server
|
|
48
114
|
this.httpsServer = plugins.https.createServer(
|
|
49
115
|
{
|
|
50
116
|
key: this.defaultCertificates.key,
|
|
51
117
|
cert: this.defaultCertificates.cert
|
|
52
118
|
},
|
|
53
|
-
(
|
|
54
|
-
this.handleRequest(originRequest, originResponse).catch((error) => {
|
|
55
|
-
console.error('Unhandled error in request handler:', error);
|
|
56
|
-
try {
|
|
57
|
-
originResponse.end();
|
|
58
|
-
} catch (err) {
|
|
59
|
-
// ignore errors during cleanup
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
},
|
|
119
|
+
(req, res) => this.handleRequest(req, res)
|
|
63
120
|
);
|
|
64
121
|
|
|
65
|
-
//
|
|
66
|
-
|
|
122
|
+
// Configure server timeouts
|
|
123
|
+
this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
|
|
124
|
+
this.httpsServer.headersTimeout = this.options.headersTimeout;
|
|
125
|
+
|
|
126
|
+
// Setup connection tracking
|
|
127
|
+
this.setupConnectionTracking();
|
|
128
|
+
|
|
129
|
+
// Setup WebSocket support
|
|
130
|
+
this.setupWebsocketSupport();
|
|
131
|
+
|
|
132
|
+
// Start metrics collection
|
|
133
|
+
this.setupMetricsCollection();
|
|
67
134
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
135
|
+
// Start the server
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
this.httpsServer.listen(this.options.port, () => {
|
|
138
|
+
this.log('info', `NetworkProxy started on port ${this.options.port}`);
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Sets up tracking of TCP connections
|
|
146
|
+
*/
|
|
147
|
+
private setupConnectionTracking(): void {
|
|
148
|
+
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
|
149
|
+
// Check if max connections reached
|
|
150
|
+
if (this.socketMap.getArray().length >= this.options.maxConnections) {
|
|
151
|
+
this.log('warn', `Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
|
|
152
|
+
connection.destroy();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add connection to tracking
|
|
157
|
+
this.socketMap.add(connection);
|
|
158
|
+
this.connectedClients = this.socketMap.getArray().length;
|
|
159
|
+
this.log('debug', `New connection. Currently ${this.connectedClients} active connections`);
|
|
160
|
+
|
|
161
|
+
// Setup connection cleanup handlers
|
|
162
|
+
const cleanupConnection = () => {
|
|
163
|
+
if (this.socketMap.checkForObject(connection)) {
|
|
164
|
+
this.socketMap.remove(connection);
|
|
165
|
+
this.connectedClients = this.socketMap.getArray().length;
|
|
166
|
+
this.log('debug', `Connection closed. ${this.connectedClients} connections remaining`);
|
|
80
167
|
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
connection.on('close', cleanupConnection);
|
|
171
|
+
connection.on('error', (err) => {
|
|
172
|
+
this.log('debug', 'Connection error', err);
|
|
173
|
+
cleanupConnection();
|
|
81
174
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
wsIncoming.lastPong = Date.now();
|
|
92
|
-
wsIncoming.on('pong', () => {
|
|
93
|
-
wsIncoming.lastPong = Date.now();
|
|
94
|
-
});
|
|
175
|
+
connection.on('end', cleanupConnection);
|
|
176
|
+
connection.on('timeout', () => {
|
|
177
|
+
this.log('debug', 'Connection timeout');
|
|
178
|
+
cleanupConnection();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
95
182
|
|
|
96
|
-
|
|
97
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Sets up WebSocket support
|
|
185
|
+
*/
|
|
186
|
+
private setupWebsocketSupport(): void {
|
|
187
|
+
// Create WebSocket server
|
|
188
|
+
this.wsServer = new plugins.ws.WebSocketServer({
|
|
189
|
+
server: this.httpsServer,
|
|
190
|
+
// Add WebSocket specific timeout
|
|
191
|
+
clientTracking: true
|
|
192
|
+
});
|
|
98
193
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
194
|
+
// Handle WebSocket connections
|
|
195
|
+
this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => {
|
|
196
|
+
this.handleWebSocketConnection(wsIncoming, reqArg);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Set up the heartbeat interval (check every 30 seconds, terminate after 2 minutes of inactivity)
|
|
200
|
+
this.heartbeatInterval = setInterval(() => {
|
|
201
|
+
if (this.wsServer.clients.size === 0) {
|
|
202
|
+
return; // Skip if no active connections
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.log('debug', `WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
|
|
206
|
+
this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
|
207
|
+
const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
|
|
208
|
+
|
|
209
|
+
if (wsWithHeartbeat.isAlive === false) {
|
|
210
|
+
this.log('debug', 'Terminating inactive WebSocket connection');
|
|
211
|
+
return wsWithHeartbeat.terminate();
|
|
104
212
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
213
|
+
|
|
214
|
+
wsWithHeartbeat.isAlive = false;
|
|
215
|
+
wsWithHeartbeat.ping();
|
|
216
|
+
});
|
|
217
|
+
}, 30000);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Sets up metrics collection
|
|
222
|
+
*/
|
|
223
|
+
private setupMetricsCollection(): void {
|
|
224
|
+
this.metricsInterval = setInterval(() => {
|
|
225
|
+
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
|
|
226
|
+
const metrics = {
|
|
227
|
+
uptime,
|
|
228
|
+
activeConnections: this.connectedClients,
|
|
229
|
+
totalRequests: this.requestsServed,
|
|
230
|
+
failedRequests: this.failedRequests,
|
|
231
|
+
activeWebSockets: this.wsServer?.clients.size || 0,
|
|
232
|
+
memoryUsage: process.memoryUsage(),
|
|
233
|
+
activeContexts: Array.from(this.activeContexts)
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
this.log('debug', 'Proxy metrics', metrics);
|
|
237
|
+
}, 60000); // Log metrics every minute
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Handles an incoming WebSocket connection
|
|
242
|
+
*/
|
|
243
|
+
private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage): void {
|
|
244
|
+
const wsPath = reqArg.url;
|
|
245
|
+
const wsHost = reqArg.headers.host;
|
|
246
|
+
|
|
247
|
+
this.log('info', `WebSocket connection for ${wsHost}${wsPath}`);
|
|
248
|
+
|
|
249
|
+
// Setup heartbeat tracking
|
|
250
|
+
wsIncoming.isAlive = true;
|
|
251
|
+
wsIncoming.lastPong = Date.now();
|
|
252
|
+
wsIncoming.on('pong', () => {
|
|
253
|
+
wsIncoming.isAlive = true;
|
|
254
|
+
wsIncoming.lastPong = Date.now();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Get the destination configuration
|
|
258
|
+
const wsDestinationConfig = this.router.routeReq(reqArg);
|
|
259
|
+
if (!wsDestinationConfig) {
|
|
260
|
+
this.log('warn', `No route found for WebSocket ${wsHost}${wsPath}`);
|
|
261
|
+
wsIncoming.terminate();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check authentication if required
|
|
266
|
+
if (wsDestinationConfig.authentication) {
|
|
267
|
+
try {
|
|
268
|
+
if (!this.authenticateRequest(reqArg, wsDestinationConfig)) {
|
|
269
|
+
this.log('warn', `WebSocket authentication failed for ${wsHost}${wsPath}`);
|
|
115
270
|
wsIncoming.terminate();
|
|
116
271
|
return;
|
|
117
272
|
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
this.log('error', 'WebSocket authentication error', error);
|
|
275
|
+
wsIncoming.terminate();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
118
279
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
wsOutgoing.send(message, { binary: isBinary });
|
|
123
|
-
} catch (error) {
|
|
124
|
-
console.error('Error sending message to wsOutgoing:', error);
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
wsOutgoing.on('message', async (message, isBinary) => {
|
|
129
|
-
try {
|
|
130
|
-
wsIncoming.send(message, { binary: isBinary });
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error('Error sending message to wsIncoming:', error);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
280
|
+
// Setup outgoing WebSocket connection
|
|
281
|
+
let wsOutgoing: plugins.wsDefault;
|
|
282
|
+
const outGoingDeferred = plugins.smartpromise.defer();
|
|
135
283
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
284
|
+
try {
|
|
285
|
+
const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`;
|
|
286
|
+
this.log('debug', `Proxying WebSocket to ${wsTarget}`);
|
|
287
|
+
|
|
288
|
+
wsOutgoing = new plugins.wsDefault(wsTarget);
|
|
289
|
+
|
|
290
|
+
wsOutgoing.on('open', () => {
|
|
291
|
+
this.log('debug', 'Outgoing WebSocket connection established');
|
|
292
|
+
outGoingDeferred.resolve();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
wsOutgoing.on('error', (error) => {
|
|
296
|
+
this.log('error', 'Outgoing WebSocket error', error);
|
|
297
|
+
outGoingDeferred.reject(error);
|
|
298
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
299
|
+
wsIncoming.terminate();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.log('error', 'Failed to create outgoing WebSocket connection', err);
|
|
304
|
+
wsIncoming.terminate();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
144
307
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
wsOutgoing.
|
|
153
|
-
|
|
154
|
-
|
|
308
|
+
// Handle message forwarding from client to backend
|
|
309
|
+
wsIncoming.on('message', async (message, isBinary) => {
|
|
310
|
+
try {
|
|
311
|
+
// Wait for outgoing connection to be ready
|
|
312
|
+
await outGoingDeferred.promise;
|
|
313
|
+
|
|
314
|
+
// Only forward if both connections are still open
|
|
315
|
+
if (wsOutgoing.readyState === wsOutgoing.OPEN) {
|
|
316
|
+
wsOutgoing.send(message, { binary: isBinary });
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.log('error', 'Error forwarding WebSocket message to backend', error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
155
322
|
|
|
156
|
-
|
|
157
|
-
|
|
323
|
+
// Handle message forwarding from backend to client
|
|
324
|
+
wsOutgoing.on('message', (message, isBinary) => {
|
|
325
|
+
try {
|
|
326
|
+
// Only forward if the incoming connection is still open
|
|
327
|
+
if (wsIncoming.readyState === wsIncoming.OPEN) {
|
|
328
|
+
wsIncoming.send(message, { binary: isBinary });
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.log('error', 'Error forwarding WebSocket message to client', error);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
158
334
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
335
|
+
// Clean up connections when either side closes
|
|
336
|
+
wsIncoming.on('close', (code, reason) => {
|
|
337
|
+
this.log('debug', `Incoming WebSocket closed: ${code} - ${reason}`);
|
|
338
|
+
if (wsOutgoing && wsOutgoing.readyState !== wsOutgoing.CLOSED) {
|
|
339
|
+
try {
|
|
340
|
+
// Validate close code (must be 1000-4999) or use 1000 as default
|
|
341
|
+
const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
|
|
342
|
+
wsOutgoing.close(validCode, reason.toString() || '');
|
|
343
|
+
} catch (error) {
|
|
344
|
+
this.log('error', 'Error closing outgoing WebSocket', error);
|
|
345
|
+
wsOutgoing.terminate();
|
|
167
346
|
}
|
|
168
|
-
}
|
|
169
|
-
connection.on('close', cleanupConnection);
|
|
170
|
-
connection.on('error', cleanupConnection);
|
|
171
|
-
connection.on('end', cleanupConnection);
|
|
172
|
-
connection.on('timeout', cleanupConnection);
|
|
347
|
+
}
|
|
173
348
|
});
|
|
174
349
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
350
|
+
wsOutgoing.on('close', (code, reason) => {
|
|
351
|
+
this.log('debug', `Outgoing WebSocket closed: ${code} - ${reason}`);
|
|
352
|
+
if (wsIncoming && wsIncoming.readyState !== wsIncoming.CLOSED) {
|
|
353
|
+
try {
|
|
354
|
+
// Validate close code (must be 1000-4999) or use 1000 as default
|
|
355
|
+
const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
|
|
356
|
+
wsIncoming.close(validCode, reason.toString() || '');
|
|
357
|
+
} catch (error) {
|
|
358
|
+
this.log('error', 'Error closing incoming WebSocket', error);
|
|
359
|
+
wsIncoming.terminate();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
179
363
|
}
|
|
180
364
|
|
|
181
365
|
/**
|
|
182
|
-
*
|
|
366
|
+
* Handles an HTTP/HTTPS request
|
|
183
367
|
*/
|
|
184
368
|
private async handleRequest(
|
|
185
369
|
originRequest: plugins.http.IncomingMessage,
|
|
186
|
-
originResponse: plugins.http.ServerResponse
|
|
370
|
+
originResponse: plugins.http.ServerResponse
|
|
187
371
|
): Promise<void> {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
372
|
+
this.requestsServed++;
|
|
373
|
+
const startTime = Date.now();
|
|
374
|
+
const reqId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const reqPath = plugins.url.parse(originRequest.url).path;
|
|
378
|
+
this.log('info', `[${reqId}] ${originRequest.method} ${originRequest.headers.host}${reqPath}`);
|
|
379
|
+
|
|
380
|
+
// Handle preflight OPTIONS requests for CORS
|
|
381
|
+
if (originRequest.method === 'OPTIONS' && this.options.cors) {
|
|
382
|
+
this.handleCorsRequest(originRequest, originResponse);
|
|
383
|
+
return;
|
|
197
384
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// authentication
|
|
215
|
-
if (destinationConfig.authentication) {
|
|
216
|
-
const authInfo = destinationConfig.authentication;
|
|
217
|
-
switch (authInfo.type) {
|
|
218
|
-
case 'Basic': {
|
|
219
|
-
const authHeader = originRequest.headers.authorization;
|
|
220
|
-
if (!authHeader) {
|
|
221
|
-
return endOriginReqRes(401, 'Authentication required', {
|
|
222
|
-
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (!authHeader.includes('Basic ')) {
|
|
226
|
-
return endOriginReqRes(401, 'Authentication required', {
|
|
227
|
-
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
|
|
385
|
+
|
|
386
|
+
// Get destination configuration
|
|
387
|
+
const destinationConfig = this.router.routeReq(originRequest);
|
|
388
|
+
if (!destinationConfig) {
|
|
389
|
+
this.log('warn', `[${reqId}] No route found for ${originRequest.headers.host}`);
|
|
390
|
+
this.sendErrorResponse(originResponse, 404, 'Not Found: No matching route');
|
|
391
|
+
this.failedRequests++;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Handle authentication if configured
|
|
396
|
+
if (destinationConfig.authentication) {
|
|
397
|
+
try {
|
|
398
|
+
if (!this.authenticateRequest(originRequest, destinationConfig)) {
|
|
399
|
+
this.sendErrorResponse(originResponse, 401, 'Unauthorized', {
|
|
400
|
+
'WWW-Authenticate': 'Basic realm="Access to the proxy site", charset="UTF-8"'
|
|
228
401
|
});
|
|
402
|
+
this.failedRequests++;
|
|
403
|
+
return;
|
|
229
404
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (user === authInfo.user && pass === authInfo.pass) {
|
|
236
|
-
console.log('Request successfully authenticated');
|
|
237
|
-
} else {
|
|
238
|
-
return endOriginReqRes(403, 'Forbidden: Wrong credentials');
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.log('error', `[${reqId}] Authentication error`, error);
|
|
407
|
+
this.sendErrorResponse(originResponse, 500, 'Internal Server Error: Authentication failed');
|
|
408
|
+
this.failedRequests++;
|
|
409
|
+
return;
|
|
241
410
|
}
|
|
242
|
-
default:
|
|
243
|
-
return endOriginReqRes(
|
|
244
|
-
403,
|
|
245
|
-
'Forbidden: unsupported authentication method configured. Please report to the admin.',
|
|
246
|
-
);
|
|
247
411
|
}
|
|
412
|
+
|
|
413
|
+
// Construct destination URL
|
|
414
|
+
const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
|
|
415
|
+
this.log('debug', `[${reqId}] Proxying to ${destinationUrl}`);
|
|
416
|
+
|
|
417
|
+
// Forward the request
|
|
418
|
+
await this.forwardRequest(reqId, originRequest, originResponse, destinationUrl);
|
|
419
|
+
|
|
420
|
+
const processingTime = Date.now() - startTime;
|
|
421
|
+
this.log('debug', `[${reqId}] Request completed in ${processingTime}ms`);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this.log('error', `[${reqId}] Unhandled error in request handler`, error);
|
|
424
|
+
try {
|
|
425
|
+
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Server error');
|
|
426
|
+
} catch (responseError) {
|
|
427
|
+
this.log('error', `[${reqId}] Failed to send error response`, responseError);
|
|
428
|
+
}
|
|
429
|
+
this.failedRequests++;
|
|
248
430
|
}
|
|
431
|
+
}
|
|
249
432
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Handles a CORS preflight request
|
|
435
|
+
*/
|
|
436
|
+
private handleCorsRequest(
|
|
437
|
+
req: plugins.http.IncomingMessage,
|
|
438
|
+
res: plugins.http.ServerResponse
|
|
439
|
+
): void {
|
|
440
|
+
const cors = this.options.cors;
|
|
441
|
+
|
|
442
|
+
// Set CORS headers
|
|
443
|
+
res.setHeader('Access-Control-Allow-Origin', cors.allowOrigin);
|
|
444
|
+
res.setHeader('Access-Control-Allow-Methods', cors.allowMethods);
|
|
445
|
+
res.setHeader('Access-Control-Allow-Headers', cors.allowHeaders);
|
|
446
|
+
res.setHeader('Access-Control-Max-Age', String(cors.maxAge));
|
|
447
|
+
|
|
448
|
+
// Handle preflight request
|
|
449
|
+
res.statusCode = 204;
|
|
450
|
+
res.end();
|
|
451
|
+
|
|
452
|
+
// Count this as a request served
|
|
453
|
+
this.requestsServed++;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Authenticates a request against the destination config
|
|
458
|
+
*/
|
|
459
|
+
private authenticateRequest(
|
|
460
|
+
req: plugins.http.IncomingMessage,
|
|
461
|
+
config: plugins.tsclass.network.IReverseProxyConfig
|
|
462
|
+
): boolean {
|
|
463
|
+
const authInfo = config.authentication;
|
|
464
|
+
if (!authInfo) {
|
|
465
|
+
return true; // No authentication required
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
switch (authInfo.type) {
|
|
469
|
+
case 'Basic': {
|
|
470
|
+
const authHeader = req.headers.authorization;
|
|
471
|
+
if (!authHeader || !authHeader.includes('Basic ')) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const authStringBase64 = authHeader.replace('Basic ', '');
|
|
476
|
+
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
|
|
477
|
+
const [user, pass] = authString.split(':');
|
|
478
|
+
|
|
479
|
+
// Use constant-time comparison to prevent timing attacks
|
|
480
|
+
const userMatch = user === authInfo.user;
|
|
481
|
+
const passMatch = pass === authInfo.pass;
|
|
482
|
+
|
|
483
|
+
return userMatch && passMatch;
|
|
484
|
+
}
|
|
485
|
+
default:
|
|
486
|
+
throw new Error(`Unsupported authentication method: ${authInfo.type}`);
|
|
255
487
|
}
|
|
256
|
-
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Forwards a request to the destination
|
|
492
|
+
*/
|
|
493
|
+
private async forwardRequest(
|
|
494
|
+
reqId: string,
|
|
495
|
+
originRequest: plugins.http.IncomingMessage,
|
|
496
|
+
originResponse: plugins.http.ServerResponse,
|
|
497
|
+
destinationUrl: string
|
|
498
|
+
): Promise<void> {
|
|
257
499
|
try {
|
|
258
|
-
const
|
|
500
|
+
const proxyRequest = await plugins.smartrequest.request(
|
|
259
501
|
destinationUrl,
|
|
260
502
|
{
|
|
261
503
|
method: originRequest.method,
|
|
262
|
-
headers:
|
|
263
|
-
...originRequest.headers,
|
|
264
|
-
'X-Forwarded-Host': originRequest.headers.host,
|
|
265
|
-
'X-Forwarded-Proto': 'https',
|
|
266
|
-
},
|
|
504
|
+
headers: this.prepareForwardHeaders(originRequest),
|
|
267
505
|
keepAlive: true,
|
|
506
|
+
timeout: 30000 // 30 second timeout
|
|
268
507
|
},
|
|
269
|
-
true, // streaming
|
|
270
|
-
(
|
|
271
|
-
originRequest.on('data', (data) => {
|
|
272
|
-
proxyRequest.write(data);
|
|
273
|
-
});
|
|
274
|
-
originRequest.on('end', () => {
|
|
275
|
-
proxyRequest.end();
|
|
276
|
-
});
|
|
277
|
-
originRequest.on('error', () => {
|
|
278
|
-
proxyRequest.end();
|
|
279
|
-
});
|
|
280
|
-
originRequest.on('close', () => {
|
|
281
|
-
proxyRequest.end();
|
|
282
|
-
});
|
|
283
|
-
originRequest.on('timeout', () => {
|
|
284
|
-
proxyRequest.end();
|
|
285
|
-
originRequest.destroy();
|
|
286
|
-
});
|
|
287
|
-
proxyRequest.on('error', () => {
|
|
288
|
-
endOriginReqRes();
|
|
289
|
-
});
|
|
290
|
-
},
|
|
508
|
+
true, // streaming
|
|
509
|
+
(proxyRequestStream) => this.setupRequestStreaming(originRequest, proxyRequestStream)
|
|
291
510
|
);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
511
|
+
|
|
512
|
+
// Handle the response
|
|
513
|
+
this.processProxyResponse(reqId, originResponse, proxyRequest);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
this.log('error', `[${reqId}] Error forwarding request`, error);
|
|
516
|
+
this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
|
|
517
|
+
throw error; // Let the main handler catch this
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Prepares headers to forward to the backend
|
|
523
|
+
*/
|
|
524
|
+
private prepareForwardHeaders(req: plugins.http.IncomingMessage): plugins.http.OutgoingHttpHeaders {
|
|
525
|
+
const safeHeaders = { ...req.headers };
|
|
526
|
+
|
|
527
|
+
// Add forwarding headers
|
|
528
|
+
safeHeaders['X-Forwarded-Host'] = req.headers.host;
|
|
529
|
+
safeHeaders['X-Forwarded-Proto'] = 'https';
|
|
530
|
+
safeHeaders['X-Forwarded-For'] = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
531
|
+
|
|
532
|
+
// Add proxy-specific headers
|
|
533
|
+
safeHeaders['X-Proxy-Id'] = `NetworkProxy-${this.options.port}`;
|
|
534
|
+
|
|
535
|
+
// Remove sensitive headers we don't want to forward
|
|
536
|
+
const sensitiveHeaders = ['connection', 'upgrade', 'http2-settings'];
|
|
537
|
+
for (const header of sensitiveHeaders) {
|
|
538
|
+
delete safeHeaders[header];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return safeHeaders;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Sets up request streaming for the proxy
|
|
546
|
+
*/
|
|
547
|
+
private setupRequestStreaming(
|
|
548
|
+
originRequest: plugins.http.IncomingMessage,
|
|
549
|
+
proxyRequest: plugins.http.ClientRequest
|
|
550
|
+
): void {
|
|
551
|
+
// Forward request body data
|
|
552
|
+
originRequest.on('data', (chunk) => {
|
|
553
|
+
proxyRequest.write(chunk);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// End the request when done
|
|
557
|
+
originRequest.on('end', () => {
|
|
558
|
+
proxyRequest.end();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Handle request errors
|
|
562
|
+
originRequest.on('error', (error) => {
|
|
563
|
+
this.log('error', 'Error in client request stream', error);
|
|
564
|
+
proxyRequest.destroy(error);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Handle client abort/timeout
|
|
568
|
+
originRequest.on('close', () => {
|
|
569
|
+
if (!originRequest.complete) {
|
|
570
|
+
this.log('debug', 'Client closed connection before request completed');
|
|
571
|
+
proxyRequest.destroy();
|
|
296
572
|
}
|
|
297
|
-
|
|
298
|
-
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
originRequest.on('timeout', () => {
|
|
576
|
+
this.log('debug', 'Client request timeout');
|
|
577
|
+
proxyRequest.destroy(new Error('Client request timeout'));
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Handle proxy request errors
|
|
581
|
+
proxyRequest.on('error', (error) => {
|
|
582
|
+
this.log('error', 'Error in outgoing proxy request', error);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Processes a proxy response
|
|
588
|
+
*/
|
|
589
|
+
private processProxyResponse(
|
|
590
|
+
reqId: string,
|
|
591
|
+
originResponse: plugins.http.ServerResponse,
|
|
592
|
+
proxyResponse: plugins.http.IncomingMessage
|
|
593
|
+
): void {
|
|
594
|
+
this.log('debug', `[${reqId}] Received upstream response: ${proxyResponse.statusCode}`);
|
|
595
|
+
|
|
596
|
+
// Set status code
|
|
597
|
+
originResponse.statusCode = proxyResponse.statusCode;
|
|
598
|
+
|
|
599
|
+
// Add default headers
|
|
600
|
+
for (const [headerName, headerValue] of Object.entries(this.defaultHeaders)) {
|
|
601
|
+
originResponse.setHeader(headerName, headerValue);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Add CORS headers if enabled
|
|
605
|
+
if (this.options.cors) {
|
|
606
|
+
originResponse.setHeader('Access-Control-Allow-Origin', this.options.cors.allowOrigin);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Copy response headers
|
|
610
|
+
for (const [headerName, headerValue] of Object.entries(proxyResponse.headers)) {
|
|
611
|
+
// Skip hop-by-hop headers
|
|
612
|
+
const hopByHopHeaders = ['connection', 'keep-alive', 'transfer-encoding', 'te',
|
|
613
|
+
'trailer', 'upgrade', 'proxy-authorization', 'proxy-authenticate'];
|
|
614
|
+
if (!hopByHopHeaders.includes(headerName.toLowerCase())) {
|
|
615
|
+
originResponse.setHeader(headerName, headerValue);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Stream response body
|
|
620
|
+
proxyResponse.on('data', (chunk) => {
|
|
621
|
+
const canContinue = originResponse.write(chunk);
|
|
622
|
+
|
|
623
|
+
// Apply backpressure if needed
|
|
624
|
+
if (!canContinue) {
|
|
625
|
+
proxyResponse.pause();
|
|
626
|
+
originResponse.once('drain', () => {
|
|
627
|
+
proxyResponse.resume();
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// End the response when done
|
|
633
|
+
proxyResponse.on('end', () => {
|
|
634
|
+
originResponse.end();
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Handle response errors
|
|
638
|
+
proxyResponse.on('error', (error) => {
|
|
639
|
+
this.log('error', `[${reqId}] Error in proxy response stream`, error);
|
|
640
|
+
originResponse.destroy(error);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
originResponse.on('error', (error) => {
|
|
644
|
+
this.log('error', `[${reqId}] Error in client response stream`, error);
|
|
645
|
+
proxyResponse.destroy();
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Sends an error response to the client
|
|
651
|
+
*/
|
|
652
|
+
private sendErrorResponse(
|
|
653
|
+
res: plugins.http.ServerResponse,
|
|
654
|
+
statusCode: number = 500,
|
|
655
|
+
message: string = 'Internal Server Error',
|
|
656
|
+
headers: plugins.http.OutgoingHttpHeaders = {}
|
|
657
|
+
): void {
|
|
658
|
+
try {
|
|
659
|
+
// If headers already sent, just end the response
|
|
660
|
+
if (res.headersSent) {
|
|
661
|
+
res.end();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Add default headers
|
|
666
|
+
for (const [key, value] of Object.entries(this.defaultHeaders)) {
|
|
667
|
+
res.setHeader(key, value);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Add provided headers
|
|
671
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
672
|
+
res.setHeader(key, value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Send error response
|
|
676
|
+
res.writeHead(statusCode, message);
|
|
677
|
+
|
|
678
|
+
// Send error body as JSON for API clients
|
|
679
|
+
if (res.getHeader('Content-Type') === 'application/json') {
|
|
680
|
+
res.end(JSON.stringify({ error: { status: statusCode, message } }));
|
|
681
|
+
} else {
|
|
682
|
+
// Send as plain text
|
|
683
|
+
res.end(message);
|
|
299
684
|
}
|
|
300
|
-
proxyResponse.on('data', (data) => {
|
|
301
|
-
originResponse.write(data);
|
|
302
|
-
});
|
|
303
|
-
proxyResponse.on('end', () => {
|
|
304
|
-
originResponse.end();
|
|
305
|
-
});
|
|
306
|
-
proxyResponse.on('error', () => {
|
|
307
|
-
originResponse.destroy();
|
|
308
|
-
});
|
|
309
|
-
proxyResponse.on('close', () => {
|
|
310
|
-
originResponse.end();
|
|
311
|
-
});
|
|
312
|
-
proxyResponse.on('timeout', () => {
|
|
313
|
-
originResponse.end();
|
|
314
|
-
originResponse.destroy();
|
|
315
|
-
});
|
|
316
685
|
} catch (error) {
|
|
317
|
-
|
|
318
|
-
|
|
686
|
+
this.log('error', 'Error sending error response', error);
|
|
687
|
+
try {
|
|
688
|
+
res.destroy();
|
|
689
|
+
} catch (destroyError) {
|
|
690
|
+
// Last resort - nothing more we can do
|
|
691
|
+
}
|
|
319
692
|
}
|
|
320
693
|
}
|
|
321
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Updates proxy configurations
|
|
697
|
+
*/
|
|
322
698
|
public async updateProxyConfigs(
|
|
323
|
-
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
|
|
324
|
-
) {
|
|
325
|
-
|
|
699
|
+
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
|
|
700
|
+
): Promise<void> {
|
|
701
|
+
this.log('info', `Updating proxy configurations (${proxyConfigsArg.length} configs)`);
|
|
702
|
+
|
|
703
|
+
// Update internal configs
|
|
326
704
|
this.proxyConfigs = proxyConfigsArg;
|
|
327
705
|
this.router.setNewProxyConfigs(proxyConfigsArg);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
706
|
+
|
|
707
|
+
// Collect all hostnames for cleanup later
|
|
708
|
+
const currentHostNames = new Set<string>();
|
|
709
|
+
|
|
710
|
+
// Add/update SSL contexts for each host
|
|
711
|
+
for (const config of proxyConfigsArg) {
|
|
712
|
+
currentHostNames.add(config.hostName);
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
// Check if we need to update the cert
|
|
716
|
+
const currentCert = this.certificateCache.get(config.hostName);
|
|
717
|
+
const shouldUpdate = !currentCert ||
|
|
718
|
+
currentCert.key !== config.privateKey ||
|
|
719
|
+
currentCert.cert !== config.publicKey;
|
|
720
|
+
|
|
721
|
+
if (shouldUpdate) {
|
|
722
|
+
this.log('debug', `Updating SSL context for ${config.hostName}`);
|
|
723
|
+
|
|
724
|
+
// Update the HTTPS server context
|
|
725
|
+
this.httpsServer.addContext(config.hostName, {
|
|
726
|
+
key: config.privateKey,
|
|
727
|
+
cert: config.publicKey
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Update the cache
|
|
731
|
+
this.certificateCache.set(config.hostName, {
|
|
732
|
+
key: config.privateKey,
|
|
733
|
+
cert: config.publicKey
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
this.activeContexts.add(config.hostName);
|
|
341
737
|
}
|
|
738
|
+
} catch (error) {
|
|
739
|
+
this.log('error', `Failed to add SSL context for ${config.hostName}`, error);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Clean up removed contexts
|
|
744
|
+
// Note: Node.js doesn't officially support removing contexts
|
|
745
|
+
// This would require server restart in production
|
|
746
|
+
for (const hostname of this.activeContexts) {
|
|
747
|
+
if (!currentHostNames.has(hostname)) {
|
|
748
|
+
this.log('info', `Hostname ${hostname} removed from configuration`);
|
|
749
|
+
this.activeContexts.delete(hostname);
|
|
750
|
+
this.certificateCache.delete(hostname);
|
|
342
751
|
}
|
|
343
|
-
|
|
344
|
-
this.httpsServer.addContext(hostCandidate.hostName, {
|
|
345
|
-
cert: hostCandidate.publicKey,
|
|
346
|
-
key: hostCandidate.privateKey,
|
|
347
|
-
});
|
|
348
752
|
}
|
|
349
753
|
}
|
|
350
754
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
755
|
+
/**
|
|
756
|
+
* Adds default headers to be included in all responses
|
|
757
|
+
*/
|
|
758
|
+
public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
|
|
759
|
+
this.log('info', 'Adding default headers', headersArg);
|
|
760
|
+
this.defaultHeaders = {
|
|
761
|
+
...this.defaultHeaders,
|
|
762
|
+
...headersArg
|
|
763
|
+
};
|
|
355
764
|
}
|
|
356
765
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Stops the proxy server
|
|
768
|
+
*/
|
|
769
|
+
public async stop(): Promise<void> {
|
|
770
|
+
this.log('info', 'Stopping NetworkProxy server');
|
|
771
|
+
|
|
772
|
+
// Clear intervals
|
|
773
|
+
if (this.heartbeatInterval) {
|
|
774
|
+
clearInterval(this.heartbeatInterval);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (this.metricsInterval) {
|
|
778
|
+
clearInterval(this.metricsInterval);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Close WebSocket server if exists
|
|
782
|
+
if (this.wsServer) {
|
|
783
|
+
for (const client of this.wsServer.clients) {
|
|
784
|
+
try {
|
|
785
|
+
client.terminate();
|
|
786
|
+
} catch (error) {
|
|
787
|
+
this.log('error', 'Error terminating WebSocket client', error);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Close all tracked sockets
|
|
362
793
|
for (const socket of this.socketMap.getArray()) {
|
|
363
|
-
|
|
794
|
+
try {
|
|
795
|
+
socket.destroy();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
this.log('error', 'Error destroying socket', error);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Close the HTTPS server
|
|
802
|
+
return new Promise((resolve) => {
|
|
803
|
+
this.httpsServer.close(() => {
|
|
804
|
+
this.log('info', 'NetworkProxy server stopped successfully');
|
|
805
|
+
resolve();
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Logs a message according to the configured log level
|
|
812
|
+
*/
|
|
813
|
+
private log(level: 'error' | 'warn' | 'info' | 'debug', message: string, data?: any): void {
|
|
814
|
+
const logLevels = {
|
|
815
|
+
error: 0,
|
|
816
|
+
warn: 1,
|
|
817
|
+
info: 2,
|
|
818
|
+
debug: 3
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// Skip if log level is higher than configured
|
|
822
|
+
if (logLevels[level] > logLevels[this.options.logLevel]) {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const timestamp = new Date().toISOString();
|
|
827
|
+
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
828
|
+
|
|
829
|
+
switch (level) {
|
|
830
|
+
case 'error':
|
|
831
|
+
console.error(`${prefix} ${message}`, data || '');
|
|
832
|
+
break;
|
|
833
|
+
case 'warn':
|
|
834
|
+
console.warn(`${prefix} ${message}`, data || '');
|
|
835
|
+
break;
|
|
836
|
+
case 'info':
|
|
837
|
+
console.log(`${prefix} ${message}`, data || '');
|
|
838
|
+
break;
|
|
839
|
+
case 'debug':
|
|
840
|
+
console.log(`${prefix} ${message}`, data || '');
|
|
841
|
+
break;
|
|
364
842
|
}
|
|
365
|
-
await done.promise;
|
|
366
|
-
clearInterval(this.heartbeatInterval);
|
|
367
|
-
console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.');
|
|
368
843
|
}
|
|
369
844
|
}
|