@push.rocks/smartproxy 5.0.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 (78) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.pp.interfaces.d.ts +23 -0
  3. package/dist_ts/classes.pp.networkproxybridge.d.ts +15 -1
  4. package/dist_ts/classes.pp.networkproxybridge.js +116 -21
  5. package/dist_ts/classes.pp.portproxy.d.ts +20 -4
  6. package/dist_ts/classes.pp.portproxy.js +321 -22
  7. package/dist_ts/index.d.ts +6 -6
  8. package/dist_ts/index.js +7 -7
  9. package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +77 -0
  10. package/dist_ts/networkproxy/classes.np.certificatemanager.js +354 -0
  11. package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +47 -0
  12. package/dist_ts/networkproxy/classes.np.connectionpool.js +210 -0
  13. package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +117 -0
  14. package/dist_ts/networkproxy/classes.np.networkproxy.js +375 -0
  15. package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +51 -0
  16. package/dist_ts/networkproxy/classes.np.requesthandler.js +210 -0
  17. package/dist_ts/networkproxy/classes.np.types.d.ts +82 -0
  18. package/dist_ts/networkproxy/classes.np.types.js +35 -0
  19. package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +38 -0
  20. package/dist_ts/networkproxy/classes.np.websockethandler.js +188 -0
  21. package/dist_ts/networkproxy/index.d.ts +6 -0
  22. package/dist_ts/networkproxy/index.js +8 -0
  23. package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +219 -0
  24. package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +1542 -0
  25. package/dist_ts/port80handler/classes.port80handler.d.ts +260 -0
  26. package/dist_ts/port80handler/classes.port80handler.js +928 -0
  27. package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +39 -0
  28. package/dist_ts/smartproxy/classes.pp.connectionhandler.js +754 -0
  29. package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +78 -0
  30. package/dist_ts/smartproxy/classes.pp.connectionmanager.js +378 -0
  31. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +55 -0
  32. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +103 -0
  33. package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +133 -0
  34. package/dist_ts/smartproxy/classes.pp.interfaces.js +2 -0
  35. package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +57 -0
  36. package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +306 -0
  37. package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +56 -0
  38. package/dist_ts/smartproxy/classes.pp.portrangemanager.js +179 -0
  39. package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +47 -0
  40. package/dist_ts/smartproxy/classes.pp.securitymanager.js +126 -0
  41. package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +153 -0
  42. package/dist_ts/smartproxy/classes.pp.snihandler.js +1053 -0
  43. package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +47 -0
  44. package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +154 -0
  45. package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +149 -0
  46. package/dist_ts/smartproxy/classes.pp.tlsalert.js +225 -0
  47. package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +57 -0
  48. package/dist_ts/smartproxy/classes.pp.tlsmanager.js +132 -0
  49. package/dist_ts/smartproxy/classes.smartproxy.d.ts +64 -0
  50. package/dist_ts/smartproxy/classes.smartproxy.js +567 -0
  51. package/package.json +1 -1
  52. package/readme.md +77 -27
  53. package/ts/00_commitinfo_data.ts +1 -1
  54. package/ts/index.ts +6 -6
  55. package/ts/networkproxy/classes.np.certificatemanager.ts +398 -0
  56. package/ts/networkproxy/classes.np.connectionpool.ts +241 -0
  57. package/ts/networkproxy/classes.np.networkproxy.ts +469 -0
  58. package/ts/networkproxy/classes.np.requesthandler.ts +278 -0
  59. package/ts/networkproxy/classes.np.types.ts +123 -0
  60. package/ts/networkproxy/classes.np.websockethandler.ts +226 -0
  61. package/ts/networkproxy/index.ts +7 -0
  62. package/ts/{classes.port80handler.ts → port80handler/classes.port80handler.ts} +249 -1
  63. package/ts/{classes.pp.connectionhandler.ts → smartproxy/classes.pp.connectionhandler.ts} +1 -1
  64. package/ts/{classes.pp.connectionmanager.ts → smartproxy/classes.pp.connectionmanager.ts} +1 -1
  65. package/ts/{classes.pp.domainconfigmanager.ts → smartproxy/classes.pp.domainconfigmanager.ts} +1 -1
  66. package/ts/{classes.pp.interfaces.ts → smartproxy/classes.pp.interfaces.ts} +31 -5
  67. package/ts/{classes.pp.networkproxybridge.ts → smartproxy/classes.pp.networkproxybridge.ts} +129 -28
  68. package/ts/{classes.pp.securitymanager.ts → smartproxy/classes.pp.securitymanager.ts} +1 -1
  69. package/ts/{classes.pp.tlsmanager.ts → smartproxy/classes.pp.tlsmanager.ts} +1 -1
  70. package/ts/smartproxy/classes.smartproxy.ts +679 -0
  71. package/ts/classes.networkproxy.ts +0 -1730
  72. package/ts/classes.pp.acmemanager.ts +0 -149
  73. package/ts/classes.pp.portproxy.ts +0 -344
  74. /package/ts/{classes.nftablesproxy.ts → nfttablesproxy/classes.nftablesproxy.ts} +0 -0
  75. /package/ts/{classes.pp.portrangemanager.ts → smartproxy/classes.pp.portrangemanager.ts} +0 -0
  76. /package/ts/{classes.pp.snihandler.ts → smartproxy/classes.pp.snihandler.ts} +0 -0
  77. /package/ts/{classes.pp.timeoutmanager.ts → smartproxy/classes.pp.timeoutmanager.ts} +0 -0
  78. /package/ts/{classes.pp.tlsalert.ts → smartproxy/classes.pp.tlsalert.ts} +0 -0
@@ -0,0 +1,241 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { type INetworkProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './classes.np.types.js';
3
+
4
+ /**
5
+ * Manages a pool of backend connections for efficient reuse
6
+ */
7
+ export class ConnectionPool {
8
+ private connectionPool: Map<string, Array<IConnectionEntry>> = new Map();
9
+ private roundRobinPositions: Map<string, number> = new Map();
10
+ private logger: ILogger;
11
+
12
+ constructor(private options: INetworkProxyOptions) {
13
+ this.logger = createLogger(options.logLevel || 'info');
14
+ }
15
+
16
+ /**
17
+ * Get a connection from the pool or create a new one
18
+ */
19
+ public getConnection(host: string, port: number): Promise<plugins.net.Socket> {
20
+ return new Promise((resolve, reject) => {
21
+ const poolKey = `${host}:${port}`;
22
+ const connectionList = this.connectionPool.get(poolKey) || [];
23
+
24
+ // Look for an idle connection
25
+ const idleConnectionIndex = connectionList.findIndex(c => c.isIdle);
26
+
27
+ if (idleConnectionIndex >= 0) {
28
+ // Get existing connection from pool
29
+ const connection = connectionList[idleConnectionIndex];
30
+ connection.isIdle = false;
31
+ connection.lastUsed = Date.now();
32
+ this.logger.debug(`Reusing connection from pool for ${poolKey}`);
33
+
34
+ // Update the pool
35
+ this.connectionPool.set(poolKey, connectionList);
36
+
37
+ resolve(connection.socket);
38
+ return;
39
+ }
40
+
41
+ // No idle connection available, create a new one if pool isn't full
42
+ const poolSize = this.options.connectionPoolSize || 50;
43
+ if (connectionList.length < poolSize) {
44
+ this.logger.debug(`Creating new connection to ${host}:${port}`);
45
+
46
+ try {
47
+ const socket = plugins.net.connect({
48
+ host,
49
+ port,
50
+ keepAlive: true,
51
+ keepAliveInitialDelay: 30000 // 30 seconds
52
+ });
53
+
54
+ socket.once('connect', () => {
55
+ // Add to connection pool
56
+ const connection = {
57
+ socket,
58
+ lastUsed: Date.now(),
59
+ isIdle: false
60
+ };
61
+
62
+ connectionList.push(connection);
63
+ this.connectionPool.set(poolKey, connectionList);
64
+
65
+ // Setup cleanup when the connection is closed
66
+ socket.once('close', () => {
67
+ const idx = connectionList.findIndex(c => c.socket === socket);
68
+ if (idx >= 0) {
69
+ connectionList.splice(idx, 1);
70
+ this.connectionPool.set(poolKey, connectionList);
71
+ this.logger.debug(`Removed closed connection from pool for ${poolKey}`);
72
+ }
73
+ });
74
+
75
+ resolve(socket);
76
+ });
77
+
78
+ socket.once('error', (err) => {
79
+ this.logger.error(`Error creating connection to ${host}:${port}`, err);
80
+ reject(err);
81
+ });
82
+ } catch (err) {
83
+ this.logger.error(`Failed to create connection to ${host}:${port}`, err);
84
+ reject(err);
85
+ }
86
+ } else {
87
+ // Pool is full, wait for an idle connection or reject
88
+ this.logger.warn(`Connection pool for ${poolKey} is full (${connectionList.length})`);
89
+ reject(new Error(`Connection pool for ${poolKey} is full`));
90
+ }
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Return a connection to the pool for reuse
96
+ */
97
+ public returnConnection(socket: plugins.net.Socket, host: string, port: number): void {
98
+ const poolKey = `${host}:${port}`;
99
+ const connectionList = this.connectionPool.get(poolKey) || [];
100
+
101
+ // Find this connection in the pool
102
+ const connectionIndex = connectionList.findIndex(c => c.socket === socket);
103
+
104
+ if (connectionIndex >= 0) {
105
+ // Mark as idle and update last used time
106
+ connectionList[connectionIndex].isIdle = true;
107
+ connectionList[connectionIndex].lastUsed = Date.now();
108
+
109
+ this.logger.debug(`Returned connection to pool for ${poolKey}`);
110
+ } else {
111
+ this.logger.warn(`Attempted to return unknown connection to pool for ${poolKey}`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Cleanup the connection pool by removing idle connections
117
+ * or reducing pool size if it exceeds the configured maximum
118
+ */
119
+ public cleanupConnectionPool(): void {
120
+ const now = Date.now();
121
+ const idleTimeout = this.options.keepAliveTimeout || 120000; // 2 minutes default
122
+
123
+ for (const [host, connections] of this.connectionPool.entries()) {
124
+ // Sort by last used time (oldest first)
125
+ connections.sort((a, b) => a.lastUsed - b.lastUsed);
126
+
127
+ // Remove idle connections older than the idle timeout
128
+ let removed = 0;
129
+ while (connections.length > 0) {
130
+ const connection = connections[0];
131
+
132
+ // Remove if idle and exceeds timeout, or if pool is too large
133
+ if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
134
+ connections.length > (this.options.connectionPoolSize || 50)) {
135
+
136
+ try {
137
+ if (!connection.socket.destroyed) {
138
+ connection.socket.end();
139
+ connection.socket.destroy();
140
+ }
141
+ } catch (err) {
142
+ this.logger.error(`Error destroying pooled connection to ${host}`, err);
143
+ }
144
+
145
+ connections.shift(); // Remove from pool
146
+ removed++;
147
+ } else {
148
+ break; // Stop removing if we've reached active or recent connections
149
+ }
150
+ }
151
+
152
+ if (removed > 0) {
153
+ this.logger.debug(`Removed ${removed} idle connections from pool for ${host}, ${connections.length} remaining`);
154
+ }
155
+
156
+ // Update the pool with the remaining connections
157
+ if (connections.length === 0) {
158
+ this.connectionPool.delete(host);
159
+ } else {
160
+ this.connectionPool.set(host, connections);
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Close all connections in the pool
167
+ */
168
+ public closeAllConnections(): void {
169
+ for (const [host, connections] of this.connectionPool.entries()) {
170
+ this.logger.debug(`Closing ${connections.length} connections to ${host}`);
171
+
172
+ for (const connection of connections) {
173
+ try {
174
+ if (!connection.socket.destroyed) {
175
+ connection.socket.end();
176
+ connection.socket.destroy();
177
+ }
178
+ } catch (error) {
179
+ this.logger.error(`Error closing connection to ${host}:`, error);
180
+ }
181
+ }
182
+ }
183
+
184
+ this.connectionPool.clear();
185
+ this.roundRobinPositions.clear();
186
+ }
187
+
188
+ /**
189
+ * Get load balancing target using round-robin
190
+ */
191
+ public getNextTarget(targets: string[], port: number): { host: string, port: number } {
192
+ const targetKey = targets.join(',');
193
+
194
+ // Initialize position if not exists
195
+ if (!this.roundRobinPositions.has(targetKey)) {
196
+ this.roundRobinPositions.set(targetKey, 0);
197
+ }
198
+
199
+ // Get current position and increment for next time
200
+ const currentPosition = this.roundRobinPositions.get(targetKey)!;
201
+ const nextPosition = (currentPosition + 1) % targets.length;
202
+ this.roundRobinPositions.set(targetKey, nextPosition);
203
+
204
+ // Return the selected target
205
+ return {
206
+ host: targets[currentPosition],
207
+ port
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Gets the connection pool status
213
+ */
214
+ public getPoolStatus(): Record<string, { total: number, idle: number }> {
215
+ return Object.fromEntries(
216
+ Array.from(this.connectionPool.entries()).map(([host, connections]) => [
217
+ host,
218
+ {
219
+ total: connections.length,
220
+ idle: connections.filter(c => c.isIdle).length
221
+ }
222
+ ])
223
+ );
224
+ }
225
+
226
+ /**
227
+ * Setup a periodic cleanup task
228
+ */
229
+ public setupPeriodicCleanup(interval: number = 60000): NodeJS.Timeout {
230
+ const timer = setInterval(() => {
231
+ this.cleanupConnectionPool();
232
+ }, interval);
233
+
234
+ // Don't prevent process exit
235
+ if (timer.unref) {
236
+ timer.unref();
237
+ }
238
+
239
+ return timer;
240
+ }
241
+ }
@@ -0,0 +1,469 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './classes.np.types.js';
3
+ import { CertificateManager } from './classes.np.certificatemanager.js';
4
+ import { ConnectionPool } from './classes.np.connectionpool.js';
5
+ import { RequestHandler, type IMetricsTracker } from './classes.np.requesthandler.js';
6
+ import { WebSocketHandler } from './classes.np.websockethandler.js';
7
+ import { ProxyRouter } from '../classes.router.js';
8
+ import { Port80Handler } from '../port80handler/classes.port80handler.js';
9
+
10
+ /**
11
+ * NetworkProxy provides a reverse proxy with TLS termination, WebSocket support,
12
+ * automatic certificate management, and high-performance connection pooling.
13
+ */
14
+ export class NetworkProxy implements IMetricsTracker {
15
+ // Configuration
16
+ public options: INetworkProxyOptions;
17
+ public proxyConfigs: IReverseProxyConfig[] = [];
18
+
19
+ // Server instances
20
+ public httpsServer: plugins.https.Server;
21
+
22
+ // Core components
23
+ private certificateManager: CertificateManager;
24
+ private connectionPool: ConnectionPool;
25
+ private requestHandler: RequestHandler;
26
+ private webSocketHandler: WebSocketHandler;
27
+ private router = new ProxyRouter();
28
+
29
+ // State tracking
30
+ public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
31
+ public activeContexts: Set<string> = new Set();
32
+ public connectedClients: number = 0;
33
+ public startTime: number = 0;
34
+ public requestsServed: number = 0;
35
+ public failedRequests: number = 0;
36
+
37
+ // Tracking for PortProxy integration
38
+ private portProxyConnections: number = 0;
39
+ private tlsTerminatedConnections: number = 0;
40
+
41
+ // Timers
42
+ private metricsInterval: NodeJS.Timeout;
43
+ private connectionPoolCleanupInterval: NodeJS.Timeout;
44
+
45
+ // Logger
46
+ private logger: ILogger;
47
+
48
+ /**
49
+ * Creates a new NetworkProxy instance
50
+ */
51
+ constructor(optionsArg: INetworkProxyOptions) {
52
+ // Set default options
53
+ this.options = {
54
+ port: optionsArg.port,
55
+ maxConnections: optionsArg.maxConnections || 10000,
56
+ keepAliveTimeout: optionsArg.keepAliveTimeout || 120000, // 2 minutes
57
+ headersTimeout: optionsArg.headersTimeout || 60000, // 1 minute
58
+ logLevel: optionsArg.logLevel || 'info',
59
+ cors: optionsArg.cors || {
60
+ allowOrigin: '*',
61
+ allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
62
+ allowHeaders: 'Content-Type, Authorization',
63
+ maxAge: 86400
64
+ },
65
+ // Defaults for PortProxy integration
66
+ connectionPoolSize: optionsArg.connectionPoolSize || 50,
67
+ portProxyIntegration: optionsArg.portProxyIntegration || false,
68
+ useExternalPort80Handler: optionsArg.useExternalPort80Handler || false,
69
+ // Default ACME options
70
+ acme: {
71
+ enabled: optionsArg.acme?.enabled || false,
72
+ port: optionsArg.acme?.port || 80,
73
+ contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
74
+ useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
75
+ renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
76
+ autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
77
+ certificateStore: optionsArg.acme?.certificateStore || './certs',
78
+ skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
79
+ }
80
+ };
81
+
82
+ // Initialize logger
83
+ this.logger = createLogger(this.options.logLevel);
84
+
85
+ // Initialize components
86
+ this.certificateManager = new CertificateManager(this.options);
87
+ this.connectionPool = new ConnectionPool(this.options);
88
+ this.requestHandler = new RequestHandler(this.options, this.connectionPool, this.router);
89
+ this.webSocketHandler = new WebSocketHandler(this.options, this.connectionPool, this.router);
90
+
91
+ // Connect request handler to this metrics tracker
92
+ this.requestHandler.setMetricsTracker(this);
93
+ }
94
+
95
+ /**
96
+ * Implements IMetricsTracker interface to increment request counters
97
+ */
98
+ public incrementRequestsServed(): void {
99
+ this.requestsServed++;
100
+ }
101
+
102
+ /**
103
+ * Implements IMetricsTracker interface to increment failed request counters
104
+ */
105
+ public incrementFailedRequests(): void {
106
+ this.failedRequests++;
107
+ }
108
+
109
+ /**
110
+ * Returns the port number this NetworkProxy is listening on
111
+ * Useful for PortProxy to determine where to forward connections
112
+ */
113
+ public getListeningPort(): number {
114
+ return this.options.port;
115
+ }
116
+
117
+ /**
118
+ * Updates the server capacity settings
119
+ * @param maxConnections Maximum number of simultaneous connections
120
+ * @param keepAliveTimeout Keep-alive timeout in milliseconds
121
+ * @param connectionPoolSize Size of the connection pool per backend
122
+ */
123
+ public updateCapacity(maxConnections?: number, keepAliveTimeout?: number, connectionPoolSize?: number): void {
124
+ if (maxConnections !== undefined) {
125
+ this.options.maxConnections = maxConnections;
126
+ this.logger.info(`Updated max connections to ${maxConnections}`);
127
+ }
128
+
129
+ if (keepAliveTimeout !== undefined) {
130
+ this.options.keepAliveTimeout = keepAliveTimeout;
131
+
132
+ if (this.httpsServer) {
133
+ this.httpsServer.keepAliveTimeout = keepAliveTimeout;
134
+ this.logger.info(`Updated keep-alive timeout to ${keepAliveTimeout}ms`);
135
+ }
136
+ }
137
+
138
+ if (connectionPoolSize !== undefined) {
139
+ this.options.connectionPoolSize = connectionPoolSize;
140
+ this.logger.info(`Updated connection pool size to ${connectionPoolSize}`);
141
+
142
+ // Clean up excess connections in the pool
143
+ this.connectionPool.cleanupConnectionPool();
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Returns current server metrics
149
+ * Useful for PortProxy to determine which NetworkProxy to use for load balancing
150
+ */
151
+ public getMetrics(): any {
152
+ return {
153
+ activeConnections: this.connectedClients,
154
+ totalRequests: this.requestsServed,
155
+ failedRequests: this.failedRequests,
156
+ portProxyConnections: this.portProxyConnections,
157
+ tlsTerminatedConnections: this.tlsTerminatedConnections,
158
+ connectionPoolSize: this.connectionPool.getPoolStatus(),
159
+ uptime: Math.floor((Date.now() - this.startTime) / 1000),
160
+ memoryUsage: process.memoryUsage(),
161
+ activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Sets an external Port80Handler for certificate management
167
+ * This allows the NetworkProxy to use a centrally managed Port80Handler
168
+ * instead of creating its own
169
+ *
170
+ * @param handler The Port80Handler instance to use
171
+ */
172
+ public setExternalPort80Handler(handler: Port80Handler): void {
173
+ // Connect it to the certificate manager
174
+ this.certificateManager.setExternalPort80Handler(handler);
175
+ }
176
+
177
+ /**
178
+ * Starts the proxy server
179
+ */
180
+ public async start(): Promise<void> {
181
+ this.startTime = Date.now();
182
+
183
+ // Initialize Port80Handler if enabled and not using external handler
184
+ if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) {
185
+ await this.certificateManager.initializePort80Handler();
186
+ }
187
+
188
+ // Create the HTTPS server
189
+ this.httpsServer = plugins.https.createServer(
190
+ {
191
+ key: this.certificateManager.getDefaultCertificates().key,
192
+ cert: this.certificateManager.getDefaultCertificates().cert,
193
+ SNICallback: (domain, cb) => this.certificateManager.handleSNI(domain, cb)
194
+ },
195
+ (req, res) => this.requestHandler.handleRequest(req, res)
196
+ );
197
+
198
+ // Configure server timeouts
199
+ this.httpsServer.keepAliveTimeout = this.options.keepAliveTimeout;
200
+ this.httpsServer.headersTimeout = this.options.headersTimeout;
201
+
202
+ // Setup connection tracking
203
+ this.setupConnectionTracking();
204
+
205
+ // Share HTTPS server with certificate manager
206
+ this.certificateManager.setHttpsServer(this.httpsServer);
207
+
208
+ // Setup WebSocket support
209
+ this.webSocketHandler.initialize(this.httpsServer);
210
+
211
+ // Start metrics collection
212
+ this.setupMetricsCollection();
213
+
214
+ // Setup connection pool cleanup interval
215
+ this.connectionPoolCleanupInterval = this.connectionPool.setupPeriodicCleanup();
216
+
217
+ // Start the server
218
+ return new Promise((resolve) => {
219
+ this.httpsServer.listen(this.options.port, () => {
220
+ this.logger.info(`NetworkProxy started on port ${this.options.port}`);
221
+ resolve();
222
+ });
223
+ });
224
+ }
225
+
226
+ /**
227
+ * Sets up tracking of TCP connections
228
+ */
229
+ private setupConnectionTracking(): void {
230
+ this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
231
+ // Check if max connections reached
232
+ if (this.socketMap.getArray().length >= this.options.maxConnections) {
233
+ this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
234
+ connection.destroy();
235
+ return;
236
+ }
237
+
238
+ // Add connection to tracking
239
+ this.socketMap.add(connection);
240
+ this.connectedClients = this.socketMap.getArray().length;
241
+
242
+ // Check for connection from PortProxy by inspecting the source port
243
+ const localPort = connection.localPort || 0;
244
+ const remotePort = connection.remotePort || 0;
245
+
246
+ // If this connection is from a PortProxy (usually indicated by it coming from localhost)
247
+ if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
248
+ this.portProxyConnections++;
249
+ this.logger.debug(`New connection from PortProxy (local: ${localPort}, remote: ${remotePort})`);
250
+ } else {
251
+ this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`);
252
+ }
253
+
254
+ // Setup connection cleanup handlers
255
+ const cleanupConnection = () => {
256
+ if (this.socketMap.checkForObject(connection)) {
257
+ this.socketMap.remove(connection);
258
+ this.connectedClients = this.socketMap.getArray().length;
259
+
260
+ // If this was a PortProxy connection, decrement the counter
261
+ if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
262
+ this.portProxyConnections--;
263
+ }
264
+
265
+ this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`);
266
+ }
267
+ };
268
+
269
+ connection.on('close', cleanupConnection);
270
+ connection.on('error', (err) => {
271
+ this.logger.debug('Connection error', err);
272
+ cleanupConnection();
273
+ });
274
+ connection.on('end', cleanupConnection);
275
+ });
276
+
277
+ // Track TLS handshake completions
278
+ this.httpsServer.on('secureConnection', (tlsSocket) => {
279
+ this.tlsTerminatedConnections++;
280
+ this.logger.debug('TLS handshake completed, connection secured');
281
+ });
282
+ }
283
+
284
+ /**
285
+ * Sets up metrics collection
286
+ */
287
+ private setupMetricsCollection(): void {
288
+ this.metricsInterval = setInterval(() => {
289
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000);
290
+ const metrics = {
291
+ uptime,
292
+ activeConnections: this.connectedClients,
293
+ totalRequests: this.requestsServed,
294
+ failedRequests: this.failedRequests,
295
+ portProxyConnections: this.portProxyConnections,
296
+ tlsTerminatedConnections: this.tlsTerminatedConnections,
297
+ activeWebSockets: this.webSocketHandler.getConnectionInfo().activeConnections,
298
+ memoryUsage: process.memoryUsage(),
299
+ activeContexts: Array.from(this.activeContexts),
300
+ connectionPool: this.connectionPool.getPoolStatus()
301
+ };
302
+
303
+ this.logger.debug('Proxy metrics', metrics);
304
+ }, 60000); // Log metrics every minute
305
+
306
+ // Don't keep process alive just for metrics
307
+ if (this.metricsInterval.unref) {
308
+ this.metricsInterval.unref();
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Updates proxy configurations
314
+ */
315
+ public async updateProxyConfigs(
316
+ proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]
317
+ ): Promise<void> {
318
+ this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`);
319
+
320
+ // Update internal configs
321
+ this.proxyConfigs = proxyConfigsArg;
322
+ this.router.setNewProxyConfigs(proxyConfigsArg);
323
+
324
+ // Collect all hostnames for cleanup later
325
+ const currentHostNames = new Set<string>();
326
+
327
+ // Add/update SSL contexts for each host
328
+ for (const config of proxyConfigsArg) {
329
+ currentHostNames.add(config.hostName);
330
+
331
+ try {
332
+ // Update certificate in cache
333
+ this.certificateManager.updateCertificateCache(
334
+ config.hostName,
335
+ config.publicKey,
336
+ config.privateKey
337
+ );
338
+
339
+ this.activeContexts.add(config.hostName);
340
+ } catch (error) {
341
+ this.logger.error(`Failed to add SSL context for ${config.hostName}`, error);
342
+ }
343
+ }
344
+
345
+ // Clean up removed contexts
346
+ for (const hostname of this.activeContexts) {
347
+ if (!currentHostNames.has(hostname)) {
348
+ this.logger.info(`Hostname ${hostname} removed from configuration`);
349
+ this.activeContexts.delete(hostname);
350
+ }
351
+ }
352
+
353
+ // Register domains with Port80Handler if available
354
+ const domainsForACME = Array.from(currentHostNames)
355
+ .filter(domain => !domain.includes('*')); // Skip wildcard domains
356
+
357
+ this.certificateManager.registerDomainsWithPort80Handler(domainsForACME);
358
+ }
359
+
360
+ /**
361
+ * Converts PortProxy domain configurations to NetworkProxy configs
362
+ * @param domainConfigs PortProxy domain configs
363
+ * @param sslKeyPair Default SSL key pair to use if not specified
364
+ * @returns Array of NetworkProxy configs
365
+ */
366
+ public convertPortProxyConfigs(
367
+ domainConfigs: Array<{
368
+ domains: string[];
369
+ targetIPs?: string[];
370
+ allowedIPs?: string[];
371
+ }>,
372
+ sslKeyPair?: { key: string; cert: string }
373
+ ): plugins.tsclass.network.IReverseProxyConfig[] {
374
+ const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
375
+
376
+ // Use default certificates if not provided
377
+ const defaultCerts = this.certificateManager.getDefaultCertificates();
378
+ const sslKey = sslKeyPair?.key || defaultCerts.key;
379
+ const sslCert = sslKeyPair?.cert || defaultCerts.cert;
380
+
381
+ for (const domainConfig of domainConfigs) {
382
+ // Each domain in the domains array gets its own config
383
+ for (const domain of domainConfig.domains) {
384
+ // Skip non-hostname patterns (like IP addresses)
385
+ if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
386
+ continue;
387
+ }
388
+
389
+ proxyConfigs.push({
390
+ hostName: domain,
391
+ destinationIps: domainConfig.targetIPs || ['localhost'],
392
+ destinationPorts: [this.options.port], // Use the NetworkProxy port
393
+ privateKey: sslKey,
394
+ publicKey: sslCert
395
+ });
396
+ }
397
+ }
398
+
399
+ this.logger.info(`Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
400
+ return proxyConfigs;
401
+ }
402
+
403
+ /**
404
+ * Adds default headers to be included in all responses
405
+ */
406
+ public async addDefaultHeaders(headersArg: { [key: string]: string }): Promise<void> {
407
+ this.logger.info('Adding default headers', headersArg);
408
+ this.requestHandler.setDefaultHeaders(headersArg);
409
+ }
410
+
411
+ /**
412
+ * Stops the proxy server
413
+ */
414
+ public async stop(): Promise<void> {
415
+ this.logger.info('Stopping NetworkProxy server');
416
+
417
+ // Clear intervals
418
+ if (this.metricsInterval) {
419
+ clearInterval(this.metricsInterval);
420
+ }
421
+
422
+ if (this.connectionPoolCleanupInterval) {
423
+ clearInterval(this.connectionPoolCleanupInterval);
424
+ }
425
+
426
+ // Stop WebSocket handler
427
+ this.webSocketHandler.shutdown();
428
+
429
+ // Close all tracked sockets
430
+ for (const socket of this.socketMap.getArray()) {
431
+ try {
432
+ socket.destroy();
433
+ } catch (error) {
434
+ this.logger.error('Error destroying socket', error);
435
+ }
436
+ }
437
+
438
+ // Close all connection pool connections
439
+ this.connectionPool.closeAllConnections();
440
+
441
+ // Stop Port80Handler if internally managed
442
+ await this.certificateManager.stopPort80Handler();
443
+
444
+ // Close the HTTPS server
445
+ return new Promise((resolve) => {
446
+ this.httpsServer.close(() => {
447
+ this.logger.info('NetworkProxy server stopped successfully');
448
+ resolve();
449
+ });
450
+ });
451
+ }
452
+
453
+ /**
454
+ * Requests a new certificate for a domain
455
+ * This can be used to manually trigger certificate issuance
456
+ * @param domain The domain to request a certificate for
457
+ * @returns A promise that resolves when the request is submitted (not when the certificate is issued)
458
+ */
459
+ public async requestCertificate(domain: string): Promise<boolean> {
460
+ return this.certificateManager.requestCertificate(domain);
461
+ }
462
+
463
+ /**
464
+ * Gets all proxy configurations currently in use
465
+ */
466
+ public getProxyConfigs(): plugins.tsclass.network.IReverseProxyConfig[] {
467
+ return [...this.proxyConfigs];
468
+ }
469
+ }