@push.rocks/smartproxy 5.1.0 → 6.0.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.
Files changed (75) hide show
  1. package/dist_ts/classes.pp.interfaces.d.ts +23 -0
  2. package/dist_ts/classes.pp.networkproxybridge.d.ts +15 -1
  3. package/dist_ts/classes.pp.networkproxybridge.js +116 -21
  4. package/dist_ts/classes.pp.portproxy.d.ts +20 -4
  5. package/dist_ts/classes.pp.portproxy.js +321 -22
  6. package/dist_ts/index.d.ts +6 -6
  7. package/dist_ts/index.js +7 -7
  8. package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +77 -0
  9. package/dist_ts/networkproxy/classes.np.certificatemanager.js +354 -0
  10. package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +47 -0
  11. package/dist_ts/networkproxy/classes.np.connectionpool.js +210 -0
  12. package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +117 -0
  13. package/dist_ts/networkproxy/classes.np.networkproxy.js +375 -0
  14. package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +51 -0
  15. package/dist_ts/networkproxy/classes.np.requesthandler.js +210 -0
  16. package/dist_ts/networkproxy/classes.np.types.d.ts +82 -0
  17. package/dist_ts/networkproxy/classes.np.types.js +35 -0
  18. package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +38 -0
  19. package/dist_ts/networkproxy/classes.np.websockethandler.js +188 -0
  20. package/dist_ts/networkproxy/index.d.ts +6 -0
  21. package/dist_ts/networkproxy/index.js +8 -0
  22. package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +219 -0
  23. package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +1542 -0
  24. package/dist_ts/port80handler/classes.port80handler.d.ts +260 -0
  25. package/dist_ts/port80handler/classes.port80handler.js +928 -0
  26. package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +39 -0
  27. package/dist_ts/smartproxy/classes.pp.connectionhandler.js +754 -0
  28. package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +78 -0
  29. package/dist_ts/smartproxy/classes.pp.connectionmanager.js +378 -0
  30. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +55 -0
  31. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +103 -0
  32. package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +133 -0
  33. package/dist_ts/smartproxy/classes.pp.interfaces.js +2 -0
  34. package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +57 -0
  35. package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +306 -0
  36. package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +56 -0
  37. package/dist_ts/smartproxy/classes.pp.portrangemanager.js +179 -0
  38. package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +47 -0
  39. package/dist_ts/smartproxy/classes.pp.securitymanager.js +126 -0
  40. package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +153 -0
  41. package/dist_ts/smartproxy/classes.pp.snihandler.js +1053 -0
  42. package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +47 -0
  43. package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +154 -0
  44. package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +149 -0
  45. package/dist_ts/smartproxy/classes.pp.tlsalert.js +225 -0
  46. package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +57 -0
  47. package/dist_ts/smartproxy/classes.pp.tlsmanager.js +132 -0
  48. package/dist_ts/smartproxy/classes.smartproxy.d.ts +64 -0
  49. package/dist_ts/smartproxy/classes.smartproxy.js +567 -0
  50. package/package.json +1 -1
  51. package/ts/index.ts +6 -6
  52. package/ts/networkproxy/classes.np.certificatemanager.ts +398 -0
  53. package/ts/networkproxy/classes.np.connectionpool.ts +241 -0
  54. package/ts/networkproxy/classes.np.networkproxy.ts +469 -0
  55. package/ts/networkproxy/classes.np.requesthandler.ts +278 -0
  56. package/ts/networkproxy/classes.np.types.ts +123 -0
  57. package/ts/networkproxy/classes.np.websockethandler.ts +226 -0
  58. package/ts/networkproxy/index.ts +7 -0
  59. package/ts/{classes.port80handler.ts → port80handler/classes.port80handler.ts} +249 -1
  60. package/ts/{classes.pp.connectionhandler.ts → smartproxy/classes.pp.connectionhandler.ts} +1 -1
  61. package/ts/{classes.pp.connectionmanager.ts → smartproxy/classes.pp.connectionmanager.ts} +1 -1
  62. package/ts/{classes.pp.domainconfigmanager.ts → smartproxy/classes.pp.domainconfigmanager.ts} +1 -1
  63. package/ts/{classes.pp.interfaces.ts → smartproxy/classes.pp.interfaces.ts} +31 -5
  64. package/ts/{classes.pp.networkproxybridge.ts → smartproxy/classes.pp.networkproxybridge.ts} +129 -28
  65. package/ts/{classes.pp.securitymanager.ts → smartproxy/classes.pp.securitymanager.ts} +1 -1
  66. package/ts/{classes.pp.tlsmanager.ts → smartproxy/classes.pp.tlsmanager.ts} +1 -1
  67. package/ts/smartproxy/classes.smartproxy.ts +679 -0
  68. package/ts/classes.networkproxy.ts +0 -1730
  69. package/ts/classes.pp.acmemanager.ts +0 -149
  70. package/ts/classes.pp.portproxy.ts +0 -344
  71. /package/ts/{classes.nftablesproxy.ts → nfttablesproxy/classes.nftablesproxy.ts} +0 -0
  72. /package/ts/{classes.pp.portrangemanager.ts → smartproxy/classes.pp.portrangemanager.ts} +0 -0
  73. /package/ts/{classes.pp.snihandler.ts → smartproxy/classes.pp.snihandler.ts} +0 -0
  74. /package/ts/{classes.pp.timeoutmanager.ts → smartproxy/classes.pp.timeoutmanager.ts} +0 -0
  75. /package/ts/{classes.pp.tlsalert.ts → smartproxy/classes.pp.tlsalert.ts} +0 -0
@@ -1,1730 +0,0 @@
1
- import * as plugins from './plugins.js';
2
- import { ProxyRouter } from './classes.router.js';
3
- import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from './classes.port80handler.js';
4
- import * as fs from 'fs';
5
- import * as path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- export interface INetworkProxyOptions {
9
- port: number;
10
- maxConnections?: number;
11
- keepAliveTimeout?: number;
12
- headersTimeout?: number;
13
- logLevel?: 'error' | 'warn' | 'info' | 'debug';
14
- cors?: {
15
- allowOrigin?: string;
16
- allowMethods?: string;
17
- allowHeaders?: string;
18
- maxAge?: number;
19
- };
20
-
21
- // New settings for PortProxy integration
22
- connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
23
- portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
24
-
25
- // ACME certificate management options
26
- acme?: {
27
- enabled?: boolean; // Whether to enable automatic certificate management
28
- port?: number; // Port to listen on for ACME challenges (default: 80)
29
- contactEmail?: string; // Email for Let's Encrypt account
30
- useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
31
- renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
32
- autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
33
- certificateStore?: string; // Directory to store certificates (default: ./certs)
34
- skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
35
- };
36
- }
37
-
38
- interface IWebSocketWithHeartbeat extends plugins.wsDefault {
39
- lastPong: number;
40
- isAlive: boolean;
41
- }
42
-
43
- export class NetworkProxy {
44
- // Configuration
45
- public options: INetworkProxyOptions;
46
- public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
47
- public defaultHeaders: { [key: string]: string } = {};
48
-
49
- // Server instances
50
- public httpsServer: plugins.https.Server;
51
- public wsServer: plugins.ws.WebSocketServer;
52
-
53
- // State tracking
54
- public router = new ProxyRouter();
55
- public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
56
- public activeContexts: Set<string> = new Set();
57
- public connectedClients: number = 0;
58
- public startTime: number = 0;
59
- public requestsServed: number = 0;
60
- public failedRequests: number = 0;
61
-
62
- // New tracking for PortProxy integration
63
- private portProxyConnections: number = 0;
64
- private tlsTerminatedConnections: number = 0;
65
-
66
- // Timers and intervals
67
- private heartbeatInterval: NodeJS.Timeout;
68
- private metricsInterval: NodeJS.Timeout;
69
- private connectionPoolCleanupInterval: NodeJS.Timeout;
70
-
71
- // Certificates
72
- private defaultCertificates: { key: string; cert: string };
73
- private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
74
-
75
- // Port80Handler for certificate management
76
- private port80Handler: Port80Handler | null = null;
77
- private certificateStoreDir: string;
78
-
79
- // New connection pool for backend connections
80
- private connectionPool: Map<string, Array<{
81
- socket: plugins.net.Socket;
82
- lastUsed: number;
83
- isIdle: boolean;
84
- }>> = new Map();
85
-
86
- // Track round-robin positions for load balancing
87
- private roundRobinPositions: Map<string, number> = new Map();
88
-
89
- /**
90
- * Creates a new NetworkProxy instance
91
- */
92
- constructor(optionsArg: INetworkProxyOptions) {
93
- // Set default options
94
- this.options = {
95
- port: optionsArg.port,
96
- maxConnections: optionsArg.maxConnections || 10000,
97
- keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes
98
- headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
99
- logLevel: optionsArg.logLevel || 'info',
100
- cors: optionsArg.cors || {
101
- allowOrigin: '*',
102
- allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
103
- allowHeaders: 'Content-Type, Authorization',
104
- maxAge: 86400
105
- },
106
- // New defaults for PortProxy integration
107
- connectionPoolSize: optionsArg.connectionPoolSize || 50,
108
- portProxyIntegration: optionsArg.portProxyIntegration || false,
109
- // Default ACME options
110
- acme: {
111
- enabled: optionsArg.acme?.enabled || false,
112
- port: optionsArg.acme?.port || 80,
113
- contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
114
- useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
115
- renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
116
- autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
117
- certificateStore: optionsArg.acme?.certificateStore || './certs',
118
- skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
119
- }
120
- };
121
-
122
- // Set up certificate store directory
123
- this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
124
-
125
- // Ensure certificate store directory exists
126
- try {
127
- if (!fs.existsSync(this.certificateStoreDir)) {
128
- fs.mkdirSync(this.certificateStoreDir, { recursive: true });
129
- this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
130
- }
131
- } catch (error) {
132
- this.log('warn', `Failed to create certificate store directory: ${error}`);
133
- }
134
-
135
- this.loadDefaultCertificates();
136
- }
137
-
138
- /**
139
- * Loads default certificates from the filesystem
140
- */
141
- private loadDefaultCertificates(): void {
142
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
143
- const certPath = path.join(__dirname, '..', 'assets', 'certs');
144
-
145
- try {
146
- this.defaultCertificates = {
147
- key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
148
- cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
149
- };
150
- this.log('info', 'Default certificates loaded successfully');
151
- } catch (error) {
152
- this.log('error', 'Error loading default certificates', error);
153
-
154
- // Generate self-signed fallback certificates
155
- try {
156
- // This is a placeholder for actual certificate generation code
157
- // In a real implementation, you would use a library like selfsigned to generate certs
158
- this.defaultCertificates = {
159
- key: "FALLBACK_KEY_CONTENT",
160
- cert: "FALLBACK_CERT_CONTENT"
161
- };
162
- this.log('warn', 'Using fallback self-signed certificates');
163
- } catch (fallbackError) {
164
- this.log('error', 'Failed to generate fallback certificates', fallbackError);
165
- throw new Error('Could not load or generate SSL certificates');
166
- }
167
- }
168
- }
169
-
170
- /**
171
- * Returns the port number this NetworkProxy is listening on
172
- * Useful for PortProxy to determine where to forward connections
173
- */
174
- public getListeningPort(): number {
175
- return this.options.port;
176
- }
177
-
178
- /**
179
- * Updates the server capacity settings
180
- * @param maxConnections Maximum number of simultaneous connections
181
- * @param keepAliveTimeout Keep-alive timeout in milliseconds
182
- * @param connectionPoolSize Size of the connection pool per backend
183
- */
184
- public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
185
- if (maxConnections !== undefined) {
186
- this.options.maxConnections = maxConnections;
187
- this.log('info', `Updated max connections to ${maxConnections}`);
188
- }
189
-
190
- if (keepAliveTimeout !== undefined) {
191
- this.options.keepAliveTimeout = keepAliveTimeout;
192
-
193
- if (this.httpsServer) {
194
- this.httpsServer.keepAliveTimeout = keepAliveTimeout;
195
- this.log('info', `Updated keep-alive timeout to ${keepAliveTimeout}ms`);
196
- }
197
- }
198
-
199
- if (connectionPoolSize !== undefined) {
200
- this.options.connectionPoolSize = connectionPoolSize;
201
- this.log('info', `Updated connection pool size to ${connectionPoolSize}`);
202
-
203
- // Cleanup excess connections in the pool if the size was reduced
204
- this.cleanupConnectionPool();
205
- }
206
- }
207
-
208
- /**
209
- * Returns current server metrics
210
- * Useful for PortProxy to determine which NetworkProxy to use for load balancing
211
- */
212
- public getMetrics(): any {
213
- return {
214
- activeConnections: this.connectedClients,
215
- totalRequests: this.requestsServed,
216
- failedRequests: this.failedRequests,
217
- portProxyConnections: this.portProxyConnections,
218
- tlsTerminatedConnections: this.tlsTerminatedConnections,
219
- connectionPoolSize: Array.from(this.connectionPool.entries()).reduce((acc, [host, connections]) => {
220
- acc[host] = connections.length;
221
- return acc;
222
- }, {} as Record<string, number>),
223
- uptime: Math.floor((Date.now() - this.startTime) / 1000),
224
- memoryUsage: process.memoryUsage(),
225
- activeWebSockets: this.wsServer?.clients.size || 0
226
- };
227
- }
228
-
229
- /**
230
- * Cleanup the connection pool by removing idle connections
231
- * or reducing pool size if it exceeds the configured maximum
232
- */
233
- private cleanupConnectionPool(): void {
234
- const now = Date.now();
235
- const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
236
-
237
- for (const [host, connections] of this.connectionPool.entries()) {
238
- // Sort by last used time (oldest first)
239
- connections.sort((a, b) => a.lastUsed - b.lastUsed);
240
-
241
- // Remove idle connections older than the idle timeout
242
- let removed = 0;
243
- while (connections.length > 0) {
244
- const connection = connections[0];
245
-
246
- // Remove if idle and exceeds timeout, or if pool is too large
247
- if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
248
- connections.length > this.options.connectionPoolSize!) {
249
-
250
- try {
251
- if (!connection.socket.destroyed) {
252
- connection.socket.end();
253
- connection.socket.destroy();
254
- }
255
- } catch (err) {
256
- this.log('error', `Error destroying pooled connection to ${host}`, err);
257
- }
258
-
259
- connections.shift(); // Remove from pool
260
- removed++;
261
- } else {
262
- break; // Stop removing if we've reached active or recent connections
263
- }
264
- }
265
-
266
- if (removed > 0) {
267
- this.log('debug', `Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
268
- }
269
-
270
- // Update the pool with the remaining connections
271
- if (connections.length === 0) {
272
- this.connectionPool.delete(host);
273
- } else {
274
- this.connectionPool.set(host, connections);
275
- }
276
- }
277
- }
278
-
279
- /**
280
- * Get a connection from the pool or create a new one
281
- */
282
- private getConnectionFromPool(host: string, port: number): Promise<plugins.net.Socket> {
283
- return new Promise((resolve, reject) => {
284
- const poolKey = `${host}:${port}`;
285
- const connectionList = this.connectionPool.get(poolKey) || [];
286
-
287
- // Look for an idle connection
288
- const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
289
-
290
- if (idleConnectionIndex >= 0) {
291
- // Get existing connection from pool
292
- const connection = connectionList[idleConnectionIndex];
293
- connection.isIdle = false;
294
- connection.lastUsed = Date.now();
295
- this.log('debug', `Reusing connection from pool for ${poolKey}`);
296
-
297
- // Update the pool
298
- this.connectionPool.set(poolKey, connectionList);
299
-
300
- resolve(connection.socket);
301
- return;
302
- }
303
-
304
- // No idle connection available, create a new one if pool isn't full
305
- if (connectionList.length < this.options.connectionPoolSize!) {
306
- this.log('debug', `Creating new connection to ${host}:${port}`);
307
-
308
- try {
309
- const socket = plugins.net.connect({
310
- host,
311
- port,
312
- keepAlive: true,
313
- keepAliveInitialDelay: 30000 // 30 seconds
314
- });
315
-
316
- socket.once('connect', () => {
317
- // Add to connection pool
318
- const connection = {
319
- socket,
320
- lastUsed: Date.now(),
321
- isIdle: false
322
- };
323
-
324
- connectionList.push(connection);
325
- this.connectionPool.set(poolKey, connectionList);
326
-
327
- // Setup cleanup when the connection is closed
328
- socket.once('close', () => {
329
- const idx = connectionList.findIndex(c => c.socket === socket);
330
- if (idx >= 0) {
331
- connectionList.splice(idx, 1);
332
- this.connectionPool.set(poolKey, connectionList);
333
- this.log('debug', `Removed closed connection from pool for ${poolKey}`);
334
- }
335
- });
336
-
337
- resolve(socket);
338
- });
339
-
340
- socket.once('error', (err) => {
341
- this.log('error', `Error creating connection to ${host}:${port}`, err);
342
- reject(err);
343
- });
344
- } catch (err) {
345
- this.log('error', `Failed to create connection to ${host}:${port}`, err);
346
- reject(err);
347
- }
348
- } else {
349
- // Pool is full, wait for an idle connection or reject
350
- this.log('warn', `Connection pool for ${poolKey} is full (${connectionList.length})`);
351
- reject(new Error(`Connection pool for ${poolKey} is full`));
352
- }
353
- });
354
- }
355
-
356
- /**
357
- * Return a connection to the pool for reuse
358
- */
359
- private returnConnectionToPool(socket: plugins.net.Socket, host: string, port: number): void {
360
- const poolKey = `${host}:${port}`;
361
- const connectionList = this.connectionPool.get(poolKey) || [];
362
-
363
- // Find this connection in the pool
364
- const connectionIndex = connectionList.findIndex(c => c.socket === socket);
365
-
366
- if (connectionIndex >= 0) {
367
- // Mark as idle and update last used time
368
- connectionList[connectionIndex].isIdle = true;
369
- connectionList[connectionIndex].lastUsed = Date.now();
370
-
371
- this.log('debug', `Returned connection to pool for ${poolKey}`);
372
- } else {
373
- this.log('warn', `Attempted to return unknown connection to pool for ${poolKey}`);
374
- }
375
- }
376
-
377
- /**
378
- * Initializes the Port80Handler for ACME certificate management
379
- * @private
380
- */
381
- private async initializePort80Handler(): Promise<void> {
382
- if (!this.options.acme.enabled) {
383
- return;
384
- }
385
-
386
- // Create certificate manager
387
- this.port80Handler = new Port80Handler({
388
- port: this.options.acme.port,
389
- contactEmail: this.options.acme.contactEmail,
390
- useProduction: this.options.acme.useProduction,
391
- renewThresholdDays: this.options.acme.renewThresholdDays,
392
- httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
393
- renewCheckIntervalHours: 24 // Check daily for renewals
394
- });
395
-
396
- // Register event handlers
397
- this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
398
- this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
399
- this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
400
- this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
401
- this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
402
- });
403
-
404
- // Start the handler
405
- try {
406
- await this.port80Handler.start();
407
- this.log('info', `Port80Handler started on port ${this.options.acme.port}`);
408
-
409
- // Add domains from proxy configs
410
- this.registerDomainsWithPort80Handler();
411
- } catch (error) {
412
- this.log('error', `Failed to start Port80Handler: ${error}`);
413
- this.port80Handler = null;
414
- }
415
- }
416
-
417
- /**
418
- * Registers domains from proxy configs with the Port80Handler
419
- * @private
420
- */
421
- private registerDomainsWithPort80Handler(): void {
422
- if (!this.port80Handler) return;
423
-
424
- // Get all hostnames from proxy configs
425
- this.proxyConfigs.forEach(config => {
426
- const hostname = config.hostName;
427
-
428
- // Skip wildcard domains - can't get certs for these with HTTP-01 validation
429
- if (hostname.includes('*')) {
430
- this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
431
- return;
432
- }
433
-
434
- // Skip domains already with certificates if configured to do so
435
- if (this.options.acme.skipConfiguredCerts) {
436
- const cachedCert = this.certificateCache.get(hostname);
437
- if (cachedCert) {
438
- this.log('info', `Skipping domain with existing certificate: ${hostname}`);
439
- return;
440
- }
441
- }
442
-
443
- // Check for existing certificate in the store
444
- const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
445
- const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
446
-
447
- try {
448
- if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
449
- // Load existing certificate and key
450
- const cert = fs.readFileSync(certPath, 'utf8');
451
- const key = fs.readFileSync(keyPath, 'utf8');
452
-
453
- // Extract expiry date from certificate if possible
454
- let expiryDate: Date | undefined;
455
- try {
456
- const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
457
- if (matches && matches[1]) {
458
- expiryDate = new Date(matches[1]);
459
- }
460
- } catch (error) {
461
- this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
462
- }
463
-
464
- // Update the certificate in the handler
465
- this.port80Handler.setCertificate(hostname, cert, key, expiryDate);
466
-
467
- // Also update our own certificate cache
468
- this.updateCertificateCache(hostname, cert, key, expiryDate);
469
-
470
- this.log('info', `Loaded existing certificate for ${hostname}`);
471
- } else {
472
- // Register the domain for certificate issuance with new domain options format
473
- const domainOptions: IDomainOptions = {
474
- domainName: hostname,
475
- sslRedirect: true,
476
- acmeMaintenance: true
477
- };
478
-
479
- this.port80Handler.addDomain(domainOptions);
480
- this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
481
- }
482
- } catch (error) {
483
- this.log('error', `Error registering domain ${hostname} with Port80Handler: ${error}`);
484
- }
485
- });
486
- }
487
-
488
- /**
489
- * Handles newly issued or renewed certificates from Port80Handler
490
- * @private
491
- */
492
- private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
493
- const { domain, certificate, privateKey, expiryDate } = data;
494
-
495
- this.log('info', `Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
496
-
497
- // Update certificate in HTTPS server
498
- this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
499
-
500
- // Save the certificate to the filesystem
501
- this.saveCertificateToStore(domain, certificate, privateKey);
502
- }
503
-
504
- /**
505
- * Handles certificate issuance failures
506
- * @private
507
- */
508
- private handleCertificateFailed(data: { domain: string; error: string }): void {
509
- this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
510
- }
511
-
512
- /**
513
- * Saves certificate and private key to the filesystem
514
- * @private
515
- */
516
- private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
517
- try {
518
- const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
519
- const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
520
-
521
- fs.writeFileSync(certPath, certificate);
522
- fs.writeFileSync(keyPath, privateKey);
523
-
524
- // Ensure private key has restricted permissions
525
- try {
526
- fs.chmodSync(keyPath, 0o600);
527
- } catch (error) {
528
- this.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
529
- }
530
-
531
- this.log('info', `Saved certificate for ${domain} to ${certPath}`);
532
- } catch (error) {
533
- this.log('error', `Failed to save certificate for ${domain}: ${error}`);
534
- }
535
- }
536
-
537
- /**
538
- * Handles SNI (Server Name Indication) for TLS connections
539
- * Used by the HTTPS server to select the correct certificate for each domain
540
- * @private
541
- */
542
- private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
543
- this.log('debug', `SNI request for domain: ${domain}`);
544
-
545
- // Check if we have a certificate for this domain
546
- const certs = this.certificateCache.get(domain);
547
-
548
- if (certs) {
549
- try {
550
- // Create TLS context with the cached certificate
551
- const context = plugins.tls.createSecureContext({
552
- key: certs.key,
553
- cert: certs.cert
554
- });
555
-
556
- this.log('debug', `Using cached certificate for ${domain}`);
557
- cb(null, context);
558
- return;
559
- } catch (err) {
560
- this.log('error', `Error creating secure context for ${domain}:`, err);
561
- }
562
- }
563
-
564
- // Check if we should trigger certificate issuance
565
- if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
566
- // Check if this domain is already registered
567
- const certData = this.port80Handler.getCertificate(domain);
568
-
569
- if (!certData) {
570
- this.log('info', `No certificate found for ${domain}, registering for issuance`);
571
-
572
- // Register with new domain options format
573
- const domainOptions: IDomainOptions = {
574
- domainName: domain,
575
- sslRedirect: true,
576
- acmeMaintenance: true
577
- };
578
-
579
- this.port80Handler.addDomain(domainOptions);
580
- }
581
- }
582
-
583
- // Fall back to default certificate
584
- try {
585
- const context = plugins.tls.createSecureContext({
586
- key: this.defaultCertificates.key,
587
- cert: this.defaultCertificates.cert
588
- });
589
-
590
- this.log('debug', `Using default certificate for ${domain}`);
591
- cb(null, context);
592
- } catch (err) {
593
- this.log('error', `Error creating default secure context:`, err);
594
- cb(new Error('Cannot create secure context'), null);
595
- }
596
- }
597
-
598
- /**
599
- * Starts the proxy server
600
- */
601
- public async start(): Promise<void> {
602
- this.startTime = Date.now();
603
-
604
- // Initialize Port80Handler if enabled
605
- if (this.options.acme.enabled) {
606
- await this.initializePort80Handler();
607
- }
608
-
609
- // Create the HTTPS server
610
- this.httpsServer = plugins.https.createServer(
611
- {
612
- key: this.defaultCertificates.key,
613
- cert: this.defaultCertificates.cert,
614
- SNICallback: (domain, cb) => this.handleSNI(domain, cb)
615
- },
616
- (req, res) => this.handleRequest(req, res)
617
- );
618
-
619
- // Configure server timeouts
620
- this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
621
- this.httpsServer.headersTimeout = this.options.headersTimeout;
622
-
623
- // Setup connection tracking
624
- this.setupConnectionTracking();
625
-
626
- // Setup WebSocket support
627
- this.setupWebsocketSupport();
628
-
629
- // Start metrics collection
630
- this.setupMetricsCollection();
631
-
632
- // Setup connection pool cleanup interval
633
- this.setupConnectionPoolCleanup();
634
-
635
- // Start the server
636
- return new Promise((resolve) => {
637
- this.httpsServer.listen(this.options.port, () => {
638
- this.log('info', `NetworkProxy started on port ${this.options.port}`);
639
- resolve();
640
- });
641
- });
642
- }
643
-
644
- /**
645
- * Sets up tracking of TCP connections
646
- */
647
- private setupConnectionTracking(): void {
648
- this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
649
- // Check if max connections reached
650
- if (this.socketMap.getArray().length >= this.options.maxConnections) {
651
- this.log('warn', `Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
652
- connection.destroy();
653
- return;
654
- }
655
-
656
- // Add connection to tracking
657
- this.socketMap.add(connection);
658
- this.connectedClients = this.socketMap.getArray().length;
659
-
660
- // Check for connection from PortProxy by inspecting the source port
661
- // This is a heuristic - in a production environment you might use a more robust method
662
- const localPort = connection.localPort;
663
- const remotePort = connection.remotePort;
664
-
665
- // If this connection is from a PortProxy (usually indicated by it coming from localhost)
666
- if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
667
- this.portProxyConnections++;
668
- this.log('debug', `New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
669
- } else {
670
- this.log('debug', `New direct connection (local: ${localPort}, remote: ${remotePort})`);
671
- }
672
-
673
- // Setup connection cleanup handlers
674
- const cleanupConnection = () => {
675
- if (this.socketMap.checkForObject(connection)) {
676
- this.socketMap.remove(connection);
677
- this.connectedClients = this.socketMap.getArray().length;
678
-
679
- // If this was a PortProxy connection, decrement the counter
680
- if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
681
- this.portProxyConnections--;
682
- }
683
-
684
- this.log('debug', `Connection closed. ${this.connectedClients} connections remaining`);
685
- }
686
- };
687
-
688
- connection.on('close', cleanupConnection);
689
- connection.on('error', (err) => {
690
- this.log('debug', 'Connection error', err);
691
- cleanupConnection();
692
- });
693
- connection.on('end', cleanupConnection);
694
- connection.on('timeout', () => {
695
- this.log('debug', 'Connection timeout');
696
- cleanupConnection();
697
- });
698
- });
699
-
700
- // Track TLS handshake completions
701
- this.httpsServer.on('secureConnection', (tlsSocket) => {
702
- this.tlsTerminatedConnections++;
703
- this.log('debug', 'TLS handshake completed, connection secured');
704
- });
705
- }
706
-
707
- /**
708
- * Sets up WebSocket support
709
- */
710
- private setupWebsocketSupport(): void {
711
- // Create WebSocket server
712
- this.wsServer = new plugins.ws.WebSocketServer({
713
- server: this.httpsServer,
714
- // Add WebSocket specific timeout
715
- clientTracking: true
716
- });
717
-
718
- // Handle WebSocket connections
719
- this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => {
720
- this.handleWebSocketConnection(wsIncoming, reqArg);
721
- });
722
-
723
- // Set up the heartbeat interval (check every 30 seconds, terminate after 2 minutes of inactivity)
724
- this.heartbeatInterval = setInterval(() => {
725
- if (this.wsServer.clients.size === 0) {
726
- return; // Skip if no active connections
727
- }
728
-
729
- this.log('debug', `WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
730
- this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
731
- const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
732
-
733
- if (wsWithHeartbeat.isAlive === false) {
734
- this.log('debug', 'Terminating inactive WebSocket connection');
735
- return wsWithHeartbeat.terminate();
736
- }
737
-
738
- wsWithHeartbeat.isAlive = false;
739
- wsWithHeartbeat.ping();
740
- });
741
- }, 30000);
742
- }
743
-
744
- /**
745
- * Sets up metrics collection
746
- */
747
- private setupMetricsCollection(): void {
748
- this.metricsInterval = setInterval(() => {
749
- const uptime = Math.floor((Date.now() - this.startTime) / 1000);
750
- const metrics = {
751
- uptime,
752
- activeConnections: this.connectedClients,
753
- totalRequests: this.requestsServed,
754
- failedRequests: this.failedRequests,
755
- portProxyConnections: this.portProxyConnections,
756
- tlsTerminatedConnections: this.tlsTerminatedConnections,
757
- activeWebSockets: this.wsServer?.clients.size || 0,
758
- memoryUsage: process.memoryUsage(),
759
- activeContexts: Array.from(this.activeContexts),
760
- connectionPool: Object.fromEntries(
761
- Array.from(this.connectionPool.entries()).map(([host, connections]) => [
762
- host,
763
- {
764
- total: connections.length,
765
- idle: connections.filter(c => c.isIdle).length
766
- }
767
- ])
768
- )
769
- };
770
-
771
- this.log('debug', 'Proxy metrics', metrics);
772
- }, 60000); // Log metrics every minute
773
- }
774
-
775
- /**
776
- * Sets up connection pool cleanup
777
- */
778
- private setupConnectionPoolCleanup(): void {
779
- // Clean up idle connections every minute
780
- this.connectionPoolCleanupInterval = setInterval(() => {
781
- this.cleanupConnectionPool();
782
- }, 60000); // 1 minute
783
- }
784
-
785
- /**
786
- * Handles an incoming WebSocket connection
787
- */
788
- private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage): void {
789
- const wsPath = reqArg.url;
790
- const wsHost = reqArg.headers.host;
791
-
792
- this.log('info', `WebSocket connection for ${wsHost}${wsPath}`);
793
-
794
- // Setup heartbeat tracking
795
- wsIncoming.isAlive = true;
796
- wsIncoming.lastPong = Date.now();
797
- wsIncoming.on('pong', () => {
798
- wsIncoming.isAlive = true;
799
- wsIncoming.lastPong = Date.now();
800
- });
801
-
802
- // Get the destination configuration
803
- const wsDestinationConfig = this.router.routeReq(reqArg);
804
- if (!wsDestinationConfig) {
805
- this.log('warn', `No route found for WebSocket ${wsHost}${wsPath}`);
806
- wsIncoming.terminate();
807
- return;
808
- }
809
-
810
- // Check authentication if required
811
- if (wsDestinationConfig.authentication) {
812
- try {
813
- if (!this.authenticateRequest(reqArg, wsDestinationConfig)) {
814
- this.log('warn', `WebSocket authentication failed for ${wsHost}${wsPath}`);
815
- wsIncoming.terminate();
816
- return;
817
- }
818
- } catch (error) {
819
- this.log('error', 'WebSocket authentication error', error);
820
- wsIncoming.terminate();
821
- return;
822
- }
823
- }
824
-
825
- // Setup outgoing WebSocket connection
826
- let wsOutgoing: plugins.wsDefault;
827
- const outGoingDeferred = plugins.smartpromise.defer();
828
-
829
- try {
830
- // Select destination IP and port for WebSocket
831
- const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
832
- const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
833
- const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
834
- this.log('debug', `Proxying WebSocket to ${wsTarget}`);
835
-
836
- wsOutgoing = new plugins.wsDefault(wsTarget);
837
-
838
- wsOutgoing.on('open', () => {
839
- this.log('debug', 'Outgoing WebSocket connection established');
840
- outGoingDeferred.resolve();
841
- });
842
-
843
- wsOutgoing.on('error', (error) => {
844
- this.log('error', 'Outgoing WebSocket error', error);
845
- outGoingDeferred.reject(error);
846
- if (wsIncoming.readyState === wsIncoming.OPEN) {
847
- wsIncoming.terminate();
848
- }
849
- });
850
- } catch (err) {
851
- this.log('error', 'Failed to create outgoing WebSocket connection', err);
852
- wsIncoming.terminate();
853
- return;
854
- }
855
-
856
- // Handle message forwarding from client to backend
857
- wsIncoming.on('message', async (message, isBinary) => {
858
- try {
859
- // Wait for outgoing connection to be ready
860
- await outGoingDeferred.promise;
861
-
862
- // Only forward if both connections are still open
863
- if (wsOutgoing.readyState === wsOutgoing.OPEN) {
864
- wsOutgoing.send(message, { binary: isBinary });
865
- }
866
- } catch (error) {
867
- this.log('error', 'Error forwarding WebSocket message to backend', error);
868
- }
869
- });
870
-
871
- // Handle message forwarding from backend to client
872
- wsOutgoing.on('message', (message, isBinary) => {
873
- try {
874
- // Only forward if the incoming connection is still open
875
- if (wsIncoming.readyState === wsIncoming.OPEN) {
876
- wsIncoming.send(message, { binary: isBinary });
877
- }
878
- } catch (error) {
879
- this.log('error', 'Error forwarding WebSocket message to client', error);
880
- }
881
- });
882
-
883
- // Clean up connections when either side closes
884
- wsIncoming.on('close', (code, reason) => {
885
- this.log('debug', `Incoming WebSocket closed: ${code} - ${reason}`);
886
- if (wsOutgoing && wsOutgoing.readyState !== wsOutgoing.CLOSED) {
887
- try {
888
- // Validate close code (must be 1000-4999) or use 1000 as default
889
- const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
890
- wsOutgoing.close(validCode, reason.toString() || '');
891
- } catch (error) {
892
- this.log('error', 'Error closing outgoing WebSocket', error);
893
- wsOutgoing.terminate();
894
- }
895
- }
896
- });
897
-
898
- wsOutgoing.on('close', (code, reason) => {
899
- this.log('debug', `Outgoing WebSocket closed: ${code} - ${reason}`);
900
- if (wsIncoming && wsIncoming.readyState !== wsIncoming.CLOSED) {
901
- try {
902
- // Validate close code (must be 1000-4999) or use 1000 as default
903
- const validCode = (code >= 1000 && code <= 4999) ? code : 1000;
904
- wsIncoming.close(validCode, reason.toString() || '');
905
- } catch (error) {
906
- this.log('error', 'Error closing incoming WebSocket', error);
907
- wsIncoming.terminate();
908
- }
909
- }
910
- });
911
- }
912
-
913
- /**
914
- * Handles an HTTP/HTTPS request
915
- */
916
- private async handleRequest(
917
- originRequest: plugins.http.IncomingMessage,
918
- originResponse: plugins.http.ServerResponse
919
- ): Promise<void> {
920
- this.requestsServed++;
921
- const startTime = Date.now();
922
- const reqId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
923
-
924
- try {
925
- const reqPath = plugins.url.parse(originRequest.url).path;
926
- this.log('info', `[${reqId}] ${originRequest.method} ${originRequest.headers.host}${reqPath}`);
927
-
928
- // Handle preflight OPTIONS requests for CORS
929
- if (originRequest.method === 'OPTIONS' && this.options.cors) {
930
- this.handleCorsRequest(originRequest, originResponse);
931
- return;
932
- }
933
-
934
- // Get destination configuration
935
- const destinationConfig = this.router.routeReq(originRequest);
936
- if (!destinationConfig) {
937
- this.log('warn', `[${reqId}] No route found for ${originRequest.headers.host}`);
938
- this.sendErrorResponse(originResponse, 404, 'Not Found: No matching route');
939
- this.failedRequests++;
940
- return;
941
- }
942
-
943
- // Handle authentication if configured
944
- if (destinationConfig.authentication) {
945
- try {
946
- if (!this.authenticateRequest(originRequest, destinationConfig)) {
947
- this.sendErrorResponse(originResponse, 401, 'Unauthorized', {
948
- 'WWW-Authenticate': 'Basic realm="Access to the proxy site", charset="UTF-8"'
949
- });
950
- this.failedRequests++;
951
- return;
952
- }
953
- } catch (error) {
954
- this.log('error', `[${reqId}] Authentication error`, error);
955
- this.sendErrorResponse(originResponse, 500, 'Internal Server Error: Authentication failed');
956
- this.failedRequests++;
957
- return;
958
- }
959
- }
960
-
961
- // Determine if we should use connection pooling
962
- const useConnectionPool = this.options.portProxyIntegration &&
963
- originRequest.socket.remoteAddress?.includes('127.0.0.1');
964
-
965
- // Select destination IP and port from the arrays
966
- const destinationIp = this.selectDestinationIp(destinationConfig);
967
- const destinationPort = this.selectDestinationPort(destinationConfig);
968
-
969
- // Construct destination URL
970
- const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
971
-
972
- if (useConnectionPool) {
973
- this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
974
- await this.forwardRequestUsingConnectionPool(
975
- reqId,
976
- originRequest,
977
- originResponse,
978
- destinationIp,
979
- destinationPort,
980
- originRequest.url
981
- );
982
- } else {
983
- this.log('debug', `[${reqId}] Proxying to ${destinationUrl}`);
984
- await this.forwardRequest(reqId, originRequest, originResponse, destinationUrl);
985
- }
986
-
987
- const processingTime = Date.now() - startTime;
988
- this.log('debug', `[${reqId}] Request completed in ${processingTime}ms`);
989
- } catch (error) {
990
- this.log('error', `[${reqId}] Unhandled error in request handler`, error);
991
- try {
992
- this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Server error');
993
- } catch (responseError) {
994
- this.log('error', `[${reqId}] Failed to send error response`, responseError);
995
- }
996
- this.failedRequests++;
997
- }
998
- }
999
-
1000
- /**
1001
- * Handles a CORS preflight request
1002
- */
1003
- private handleCorsRequest(
1004
- req: plugins.http.IncomingMessage,
1005
- res: plugins.http.ServerResponse
1006
- ): void {
1007
- const cors = this.options.cors;
1008
-
1009
- // Set CORS headers
1010
- res.setHeader('Access-Control-Allow-Origin', cors.allowOrigin);
1011
- res.setHeader('Access-Control-Allow-Methods', cors.allowMethods);
1012
- res.setHeader('Access-Control-Allow-Headers', cors.allowHeaders);
1013
- res.setHeader('Access-Control-Max-Age', String(cors.maxAge));
1014
-
1015
- // Handle preflight request
1016
- res.statusCode = 204;
1017
- res.end();
1018
-
1019
- // Count this as a request served
1020
- this.requestsServed++;
1021
- }
1022
-
1023
- /**
1024
- * Authenticates a request against the destination config
1025
- */
1026
- private authenticateRequest(
1027
- req: plugins.http.IncomingMessage,
1028
- config: plugins.tsclass.network.IReverseProxyConfig
1029
- ): boolean {
1030
- const authInfo = config.authentication;
1031
- if (!authInfo) {
1032
- return true; // No authentication required
1033
- }
1034
-
1035
- switch (authInfo.type) {
1036
- case 'Basic': {
1037
- const authHeader = req.headers.authorization;
1038
- if (!authHeader || !authHeader.includes('Basic ')) {
1039
- return false;
1040
- }
1041
-
1042
- const authStringBase64 = authHeader.replace('Basic ', '');
1043
- const authString: string = plugins.smartstring.base64.decode(authStringBase64);
1044
- const [user, pass] = authString.split(':');
1045
-
1046
- // Use constant-time comparison to prevent timing attacks
1047
- const userMatch = user === authInfo.user;
1048
- const passMatch = pass === authInfo.pass;
1049
-
1050
- return userMatch && passMatch;
1051
- }
1052
- default:
1053
- throw new Error(`Unsupported authentication method: ${authInfo.type}`);
1054
- }
1055
- }
1056
-
1057
- /**
1058
- * Forwards a request to the destination using connection pool
1059
- * for optimized connection reuse from PortProxy
1060
- */
1061
- private async forwardRequestUsingConnectionPool(
1062
- reqId: string,
1063
- originRequest: plugins.http.IncomingMessage,
1064
- originResponse: plugins.http.ServerResponse,
1065
- host: string,
1066
- port: number,
1067
- path: string
1068
- ): Promise<void> {
1069
- try {
1070
- // Try to get a connection from the pool
1071
- const socket = await this.getConnectionFromPool(host, port);
1072
-
1073
- // Create an HTTP client request using the pooled socket
1074
- const reqOptions = {
1075
- createConnection: () => socket,
1076
- host,
1077
- port,
1078
- path,
1079
- method: originRequest.method,
1080
- headers: this.prepareForwardHeaders(originRequest),
1081
- timeout: 30000 // 30 second timeout
1082
- };
1083
-
1084
- const proxyReq = plugins.http.request(reqOptions);
1085
-
1086
- // Handle timeouts
1087
- proxyReq.on('timeout', () => {
1088
- this.log('warn', `[${reqId}] Request to ${host}:${port}${path} timed out`);
1089
- proxyReq.destroy();
1090
- });
1091
-
1092
- // Handle errors
1093
- proxyReq.on('error', (err) => {
1094
- this.log('error', `[${reqId}] Error in proxy request to ${host}:${port}${path}`, err);
1095
-
1096
- // Check if the client response is still writable
1097
- if (!originResponse.writableEnded) {
1098
- this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Error communicating with upstream server');
1099
- }
1100
-
1101
- // Don't return the socket to the pool on error
1102
- try {
1103
- if (!socket.destroyed) {
1104
- socket.destroy();
1105
- }
1106
- } catch (socketErr) {
1107
- this.log('error', `[${reqId}] Error destroying socket after request error`, socketErr);
1108
- }
1109
- });
1110
-
1111
- // Forward request body
1112
- originRequest.pipe(proxyReq);
1113
-
1114
- // Handle response
1115
- proxyReq.on('response', (proxyRes) => {
1116
- // Copy status and headers
1117
- originResponse.statusCode = proxyRes.statusCode;
1118
-
1119
- for (const [name, value] of Object.entries(proxyRes.headers)) {
1120
- if (value !== undefined) {
1121
- originResponse.setHeader(name, value);
1122
- }
1123
- }
1124
-
1125
- // Forward the response body
1126
- proxyRes.pipe(originResponse);
1127
-
1128
- // Return connection to pool when the response completes
1129
- proxyRes.on('end', () => {
1130
- if (!socket.destroyed) {
1131
- this.returnConnectionToPool(socket, host, port);
1132
- }
1133
- });
1134
-
1135
- proxyRes.on('error', (err) => {
1136
- this.log('error', `[${reqId}] Error in proxy response from ${host}:${port}${path}`, err);
1137
-
1138
- // Don't return the socket to the pool on error
1139
- try {
1140
- if (!socket.destroyed) {
1141
- socket.destroy();
1142
- }
1143
- } catch (socketErr) {
1144
- this.log('error', `[${reqId}] Error destroying socket after response error`, socketErr);
1145
- }
1146
- });
1147
- });
1148
- } catch (error) {
1149
- this.log('error', `[${reqId}] Error setting up pooled connection to ${host}:${port}`, error);
1150
- this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
1151
- throw error;
1152
- }
1153
- }
1154
-
1155
- /**
1156
- * Forwards a request to the destination (standard method)
1157
- */
1158
- private async forwardRequest(
1159
- reqId: string,
1160
- originRequest: plugins.http.IncomingMessage,
1161
- originResponse: plugins.http.ServerResponse,
1162
- destinationUrl: string
1163
- ): Promise<void> {
1164
- try {
1165
- const proxyRequest = await plugins.smartrequest.request(
1166
- destinationUrl,
1167
- {
1168
- method: originRequest.method,
1169
- headers: this.prepareForwardHeaders(originRequest),
1170
- keepAlive: true,
1171
- timeout: 30000 // 30 second timeout
1172
- },
1173
- true, // streaming
1174
- (proxyRequestStream) => this.setupRequestStreaming(originRequest, proxyRequestStream)
1175
- );
1176
-
1177
- // Handle the response
1178
- this.processProxyResponse(reqId, originResponse, proxyRequest);
1179
- } catch (error) {
1180
- this.log('error', `[${reqId}] Error forwarding request`, error);
1181
- this.sendErrorResponse(originResponse, 502, 'Bad Gateway: Unable to reach upstream server');
1182
- throw error; // Let the main handler catch this
1183
- }
1184
- }
1185
-
1186
- /**
1187
- * Prepares headers to forward to the backend
1188
- */
1189
- private prepareForwardHeaders(req: plugins.http.IncomingMessage): plugins.http.OutgoingHttpHeaders {
1190
- const safeHeaders = { ...req.headers };
1191
-
1192
- // Add forwarding headers
1193
- safeHeaders['X-Forwarded-Host'] = req.headers.host;
1194
- safeHeaders['X-Forwarded-Proto'] = 'https';
1195
- safeHeaders['X-Forwarded-For'] = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
1196
-
1197
- // Add proxy-specific headers
1198
- safeHeaders['X-Proxy-Id'] = `NetworkProxy-${this.options.port}`;
1199
-
1200
- // If this is coming from PortProxy, add a header to indicate that
1201
- if (this.options.portProxyIntegration && req.socket.remoteAddress?.includes('127.0.0.1')) {
1202
- safeHeaders['X-PortProxy-Forwarded'] = 'true';
1203
- }
1204
-
1205
- // Remove sensitive headers we don't want to forward
1206
- const sensitiveHeaders = ['connection', 'upgrade', 'http2-settings'];
1207
- for (const header of sensitiveHeaders) {
1208
- delete safeHeaders[header];
1209
- }
1210
-
1211
- return safeHeaders;
1212
- }
1213
-
1214
- /**
1215
- * Sets up request streaming for the proxy
1216
- */
1217
- private setupRequestStreaming(
1218
- originRequest: plugins.http.IncomingMessage,
1219
- proxyRequest: plugins.http.ClientRequest
1220
- ): void {
1221
- // Forward request body data
1222
- originRequest.on('data', (chunk) => {
1223
- proxyRequest.write(chunk);
1224
- });
1225
-
1226
- // End the request when done
1227
- originRequest.on('end', () => {
1228
- proxyRequest.end();
1229
- });
1230
-
1231
- // Handle request errors
1232
- originRequest.on('error', (error) => {
1233
- this.log('error', 'Error in client request stream', error);
1234
- proxyRequest.destroy(error);
1235
- });
1236
-
1237
- // Handle client abort/timeout
1238
- originRequest.on('close', () => {
1239
- if (!originRequest.complete) {
1240
- this.log('debug', 'Client closed connection before request completed');
1241
- proxyRequest.destroy();
1242
- }
1243
- });
1244
-
1245
- originRequest.on('timeout', () => {
1246
- this.log('debug', 'Client request timeout');
1247
- proxyRequest.destroy(new Error('Client request timeout'));
1248
- });
1249
-
1250
- // Handle proxy request errors
1251
- proxyRequest.on('error', (error) => {
1252
- this.log('error', 'Error in outgoing proxy request', error);
1253
- });
1254
- }
1255
-
1256
- /**
1257
- * Processes a proxy response
1258
- */
1259
- private processProxyResponse(
1260
- reqId: string,
1261
- originResponse: plugins.http.ServerResponse,
1262
- proxyResponse: plugins.http.IncomingMessage
1263
- ): void {
1264
- this.log('debug', `[${reqId}] Received upstream response: ${proxyResponse.statusCode}`);
1265
-
1266
- // Set status code
1267
- originResponse.statusCode = proxyResponse.statusCode;
1268
-
1269
- // Add default headers
1270
- for (const [headerName, headerValue] of Object.entries(this.defaultHeaders)) {
1271
- originResponse.setHeader(headerName, headerValue);
1272
- }
1273
-
1274
- // Add CORS headers if enabled
1275
- if (this.options.cors) {
1276
- originResponse.setHeader('Access-Control-Allow-Origin', this.options.cors.allowOrigin);
1277
- }
1278
-
1279
- // Copy response headers
1280
- for (const [headerName, headerValue] of Object.entries(proxyResponse.headers)) {
1281
- // Skip hop-by-hop headers
1282
- const hopByHopHeaders = ['connection', 'keep-alive', 'transfer-encoding', 'te',
1283
- 'trailer', 'upgrade', 'proxy-authorization', 'proxy-authenticate'];
1284
- if (!hopByHopHeaders.includes(headerName.toLowerCase())) {
1285
- originResponse.setHeader(headerName, headerValue);
1286
- }
1287
- }
1288
-
1289
- // Stream response body
1290
- proxyResponse.on('data', (chunk) => {
1291
- const canContinue = originResponse.write(chunk);
1292
-
1293
- // Apply backpressure if needed
1294
- if (!canContinue) {
1295
- proxyResponse.pause();
1296
- originResponse.once('drain', () => {
1297
- proxyResponse.resume();
1298
- });
1299
- }
1300
- });
1301
-
1302
- // End the response when done
1303
- proxyResponse.on('end', () => {
1304
- originResponse.end();
1305
- });
1306
-
1307
- // Handle response errors
1308
- proxyResponse.on('error', (error) => {
1309
- this.log('error', `[${reqId}] Error in proxy response stream`, error);
1310
- originResponse.destroy(error);
1311
- });
1312
-
1313
- originResponse.on('error', (error) => {
1314
- this.log('error', `[${reqId}] Error in client response stream`, error);
1315
- proxyResponse.destroy();
1316
- });
1317
- }
1318
-
1319
- /**
1320
- * Sends an error response to the client
1321
- */
1322
- private sendErrorResponse(
1323
- res: plugins.http.ServerResponse,
1324
- statusCode: number = 500,
1325
- message: string = 'Internal Server Error',
1326
- headers: plugins.http.OutgoingHttpHeaders = {}
1327
- ): void {
1328
- try {
1329
- // If headers already sent, just end the response
1330
- if (res.headersSent) {
1331
- res.end();
1332
- return;
1333
- }
1334
-
1335
- // Add default headers
1336
- for (const [key, value] of Object.entries(this.defaultHeaders)) {
1337
- res.setHeader(key, value);
1338
- }
1339
-
1340
- // Add provided headers
1341
- for (const [key, value] of Object.entries(headers)) {
1342
- res.setHeader(key, value);
1343
- }
1344
-
1345
- // Send error response
1346
- res.writeHead(statusCode, message);
1347
-
1348
- // Send error body as JSON for API clients
1349
- if (res.getHeader('Content-Type') === 'application/json') {
1350
- res.end(JSON.stringify({ error: { status: statusCode, message } }));
1351
- } else {
1352
- // Send as plain text
1353
- res.end(message);
1354
- }
1355
- } catch (error) {
1356
- this.log('error', 'Error sending error response', error);
1357
- try {
1358
- res.destroy();
1359
- } catch (destroyError) {
1360
- // Last resort - nothing more we can do
1361
- }
1362
- }
1363
- }
1364
-
1365
- /**
1366
- * Selects a destination IP from the array using round-robin
1367
- * @param config The proxy configuration
1368
- * @returns A destination IP address
1369
- */
1370
- private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
1371
- // For array-based configs
1372
- if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
1373
- // Get the current position or initialize it
1374
- const key = `ip_${config.hostName}`;
1375
- let position = this.roundRobinPositions.get(key) || 0;
1376
-
1377
- // Select the IP using round-robin
1378
- const selectedIp = config.destinationIps[position];
1379
-
1380
- // Update the position for next time
1381
- position = (position + 1) % config.destinationIps.length;
1382
- this.roundRobinPositions.set(key, position);
1383
-
1384
- return selectedIp;
1385
- }
1386
-
1387
- // For backward compatibility with test suites that rely on specific behavior
1388
- // Check if there's a proxyConfigs entry that matches this hostname
1389
- const matchingConfig = this.proxyConfigs.find(cfg =>
1390
- cfg.hostName === config.hostName &&
1391
- (cfg as any).destinationIp
1392
- );
1393
-
1394
- if (matchingConfig) {
1395
- return (matchingConfig as any).destinationIp;
1396
- }
1397
-
1398
- // Fallback to localhost
1399
- return 'localhost';
1400
- }
1401
-
1402
- /**
1403
- * Selects a destination port from the array using round-robin
1404
- * @param config The proxy configuration
1405
- * @returns A destination port number
1406
- */
1407
- private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
1408
- // For array-based configs
1409
- if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
1410
- // Get the current position or initialize it
1411
- const key = `port_${config.hostName}`;
1412
- let position = this.roundRobinPositions.get(key) || 0;
1413
-
1414
- // Select the port using round-robin
1415
- const selectedPort = config.destinationPorts[position];
1416
-
1417
- // Update the position for next time
1418
- position = (position + 1) % config.destinationPorts.length;
1419
- this.roundRobinPositions.set(key, position);
1420
-
1421
- return selectedPort;
1422
- }
1423
-
1424
- // For backward compatibility with test suites that rely on specific behavior
1425
- // Check if there's a proxyConfigs entry that matches this hostname
1426
- const matchingConfig = this.proxyConfigs.find(cfg =>
1427
- cfg.hostName === config.hostName &&
1428
- (cfg as any).destinationPort
1429
- );
1430
-
1431
- if (matchingConfig) {
1432
- return parseInt((matchingConfig as any).destinationPort, 10);
1433
- }
1434
-
1435
- // Fallback to port 80
1436
- return 80;
1437
- }
1438
-
1439
- /**
1440
- * Updates proxy configurations
1441
- */
1442
- public async updateProxyConfigs(
1443
- proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
1444
- ): Promise<void> {
1445
- this.log('info', `Updating proxy configurations (${proxyConfigsArg.length} configs)`);
1446
-
1447
- // Update internal configs
1448
- this.proxyConfigs = proxyConfigsArg;
1449
- this.router.setNewProxyConfigs(proxyConfigsArg);
1450
-
1451
- // Collect all hostnames for cleanup later
1452
- const currentHostNames = new Set<string>();
1453
-
1454
- // Add/update SSL contexts for each host
1455
- for (const config of proxyConfigsArg) {
1456
- currentHostNames.add(config.hostName);
1457
-
1458
- try {
1459
- // Check if we need to update the cert
1460
- const currentCert = this.certificateCache.get(config.hostName);
1461
- const shouldUpdate = !currentCert ||
1462
- currentCert.key !== config.privateKey ||
1463
- currentCert.cert !== config.publicKey;
1464
-
1465
- if (shouldUpdate) {
1466
- this.log('debug', `Updating SSL context for ${config.hostName}`);
1467
-
1468
- // Update the HTTPS server context
1469
- this.httpsServer.addContext(config.hostName, {
1470
- key: config.privateKey,
1471
- cert: config.publicKey
1472
- });
1473
-
1474
- // Update the cache
1475
- this.certificateCache.set(config.hostName, {
1476
- key: config.privateKey,
1477
- cert: config.publicKey
1478
- });
1479
-
1480
- this.activeContexts.add(config.hostName);
1481
- }
1482
- } catch (error) {
1483
- this.log('error', `Failed to add SSL context for ${config.hostName}`, error);
1484
- }
1485
- }
1486
-
1487
- // Clean up removed contexts
1488
- // Note: Node.js doesn't officially support removing contexts
1489
- // This would require server restart in production
1490
- for (const hostname of this.activeContexts) {
1491
- if (!currentHostNames.has(hostname)) {
1492
- this.log('info', `Hostname ${hostname} removed from configuration`);
1493
- this.activeContexts.delete(hostname);
1494
- this.certificateCache.delete(hostname);
1495
- }
1496
- }
1497
- }
1498
-
1499
- /**
1500
- * Converts PortProxy domain configurations to NetworkProxy configs
1501
- * @param domainConfigs PortProxy domain configs
1502
- * @param sslKeyPair Default SSL key pair to use if not specified
1503
- * @returns Array of NetworkProxy configs
1504
- */
1505
- public convertPortProxyConfigs(
1506
- domainConfigs: Array<{
1507
- domains: string[];
1508
- targetIPs?: string[];
1509
- allowedIPs?: string[];
1510
- }>,
1511
- sslKeyPair?: { key: string; cert: string }
1512
- ): plugins.tsclass.network.IReverseProxyConfig[] {
1513
- const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
1514
-
1515
- // Use default certificates if not provided
1516
- const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
1517
- const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
1518
-
1519
- for (const domainConfig of domainConfigs) {
1520
- // Each domain in the domains array gets its own config
1521
- for (const domain of domainConfig.domains) {
1522
- // Skip non-hostname patterns (like IP addresses)
1523
- if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
1524
- continue;
1525
- }
1526
-
1527
- proxyConfigs.push({
1528
- hostName: domain,
1529
- destinationIps: domainConfig.targetIPs || ['localhost'],
1530
- destinationPorts: [this.options.port], // Use the NetworkProxy port
1531
- privateKey: sslKey,
1532
- publicKey: sslCert
1533
- });
1534
- }
1535
- }
1536
-
1537
- this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
1538
- return proxyConfigs;
1539
- }
1540
-
1541
- /**
1542
- * Adds default headers to be included in all responses
1543
- */
1544
- public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
1545
- this.log('info', 'Adding default headers', headersArg);
1546
- this.defaultHeaders = {
1547
- ...this.defaultHeaders,
1548
- ...headersArg
1549
- };
1550
- }
1551
-
1552
- /**
1553
- * Stops the proxy server
1554
- */
1555
- public async stop(): Promise<void> {
1556
- this.log('info', 'Stopping NetworkProxy server');
1557
-
1558
- // Clear intervals
1559
- if (this.heartbeatInterval) {
1560
- clearInterval(this.heartbeatInterval);
1561
- }
1562
-
1563
- if (this.metricsInterval) {
1564
- clearInterval(this.metricsInterval);
1565
- }
1566
-
1567
- if (this.connectionPoolCleanupInterval) {
1568
- clearInterval(this.connectionPoolCleanupInterval);
1569
- }
1570
-
1571
- // Close WebSocket server if exists
1572
- if (this.wsServer) {
1573
- for (const client of this.wsServer.clients) {
1574
- try {
1575
- client.terminate();
1576
- } catch (error) {
1577
- this.log('error', 'Error terminating WebSocket client', error);
1578
- }
1579
- }
1580
- }
1581
-
1582
- // Close all tracked sockets
1583
- for (const socket of this.socketMap.getArray()) {
1584
- try {
1585
- socket.destroy();
1586
- } catch (error) {
1587
- this.log('error', 'Error destroying socket', error);
1588
- }
1589
- }
1590
-
1591
- // Close all connection pool connections
1592
- for (const [host, connections] of this.connectionPool.entries()) {
1593
- for (const connection of connections) {
1594
- try {
1595
- if (!connection.socket.destroyed) {
1596
- connection.socket.destroy();
1597
- }
1598
- } catch (error) {
1599
- this.log('error', `Error destroying pooled connection to ${host}`, error);
1600
- }
1601
- }
1602
- }
1603
- this.connectionPool.clear();
1604
-
1605
- // Stop Port80Handler if it's running
1606
- if (this.port80Handler) {
1607
- try {
1608
- await this.port80Handler.stop();
1609
- this.log('info', 'Port80Handler stopped');
1610
- } catch (error) {
1611
- this.log('error', 'Error stopping Port80Handler', error);
1612
- }
1613
- }
1614
-
1615
- // Close the HTTPS server
1616
- return new Promise((resolve) => {
1617
- this.httpsServer.close(() => {
1618
- this.log('info', 'NetworkProxy server stopped successfully');
1619
- resolve();
1620
- });
1621
- });
1622
- }
1623
-
1624
- /**
1625
- * Requests a new certificate for a domain
1626
- * This can be used to manually trigger certificate issuance
1627
- * @param domain The domain to request a certificate for
1628
- * @returns A promise that resolves when the request is submitted (not when the certificate is issued)
1629
- */
1630
- public async requestCertificate(domain: string): Promise<boolean> {
1631
- if (!this.options.acme.enabled) {
1632
- this.log('warn', 'ACME certificate management is not enabled');
1633
- return false;
1634
- }
1635
-
1636
- if (!this.port80Handler) {
1637
- this.log('error', 'Port80Handler is not initialized');
1638
- return false;
1639
- }
1640
-
1641
- // Skip wildcard domains - can't get certs for these with HTTP-01 validation
1642
- if (domain.includes('*')) {
1643
- this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
1644
- return false;
1645
- }
1646
-
1647
- try {
1648
- // Use the new domain options format
1649
- const domainOptions: IDomainOptions = {
1650
- domainName: domain,
1651
- sslRedirect: true,
1652
- acmeMaintenance: true
1653
- };
1654
-
1655
- this.port80Handler.addDomain(domainOptions);
1656
- this.log('info', `Certificate request submitted for domain: ${domain}`);
1657
- return true;
1658
- } catch (error) {
1659
- this.log('error', `Error requesting certificate for domain ${domain}:`, error);
1660
- return false;
1661
- }
1662
- }
1663
-
1664
- /**
1665
- * Updates the certificate cache for a domain
1666
- * @param domain The domain name
1667
- * @param certificate The certificate (PEM format)
1668
- * @param privateKey The private key (PEM format)
1669
- * @param expiryDate Optional expiry date
1670
- */
1671
- private updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
1672
- // Update certificate context in HTTPS server if it's running
1673
- if (this.httpsServer) {
1674
- try {
1675
- this.httpsServer.addContext(domain, {
1676
- key: privateKey,
1677
- cert: certificate
1678
- });
1679
- this.log('debug', `Updated SSL context for domain: ${domain}`);
1680
- } catch (error) {
1681
- this.log('error', `Error updating SSL context for domain ${domain}:`, error);
1682
- }
1683
- }
1684
-
1685
- // Update certificate in cache
1686
- this.certificateCache.set(domain, {
1687
- key: privateKey,
1688
- cert: certificate,
1689
- expires: expiryDate
1690
- });
1691
-
1692
- // Add to active contexts set
1693
- this.activeContexts.add(domain);
1694
- }
1695
-
1696
- /**
1697
- * Logs a message according to the configured log level
1698
- */
1699
- private log(level: 'error' | 'warn' | 'info' | 'debug', message: string, data?: any): void {
1700
- const logLevels = {
1701
- error: 0,
1702
- warn: 1,
1703
- info: 2,
1704
- debug: 3
1705
- };
1706
-
1707
- // Skip if log level is higher than configured
1708
- if (logLevels[level] > logLevels[this.options.logLevel]) {
1709
- return;
1710
- }
1711
-
1712
- const timestamp = new Date().toISOString();
1713
- const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
1714
-
1715
- switch (level) {
1716
- case 'error':
1717
- console.error(`${prefix} ${message}`, data || '');
1718
- break;
1719
- case 'warn':
1720
- console.warn(`${prefix} ${message}`, data || '');
1721
- break;
1722
- case 'info':
1723
- console.log(`${prefix} ${message}`, data || '');
1724
- break;
1725
- case 'debug':
1726
- console.log(`${prefix} ${message}`, data || '');
1727
- break;
1728
- }
1729
- }
1730
- }