@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
@@ -0,0 +1,398 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
6
+ import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js';
7
+
8
+ /**
9
+ * Manages SSL certificates for NetworkProxy including ACME integration
10
+ */
11
+ export class CertificateManager {
12
+ private defaultCertificates: { key: string; cert: string };
13
+ private certificateCache: Map<string, ICertificateEntry> = new Map();
14
+ private port80Handler: Port80Handler | null = null;
15
+ private externalPort80Handler: boolean = false;
16
+ private certificateStoreDir: string;
17
+ private logger: ILogger;
18
+ private httpsServer: plugins.https.Server | null = null;
19
+
20
+ constructor(private options: INetworkProxyOptions) {
21
+ this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
22
+ this.logger = createLogger(options.logLevel || 'info');
23
+
24
+ // Ensure certificate store directory exists
25
+ try {
26
+ if (!fs.existsSync(this.certificateStoreDir)) {
27
+ fs.mkdirSync(this.certificateStoreDir, { recursive: true });
28
+ this.logger.info(`Created certificate store directory: ${this.certificateStoreDir}`);
29
+ }
30
+ } catch (error) {
31
+ this.logger.warn(`Failed to create certificate store directory: ${error}`);
32
+ }
33
+
34
+ this.loadDefaultCertificates();
35
+ }
36
+
37
+ /**
38
+ * Loads default certificates from the filesystem
39
+ */
40
+ public loadDefaultCertificates(): void {
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+ const certPath = path.join(__dirname, '..', '..', 'assets', 'certs');
43
+
44
+ try {
45
+ this.defaultCertificates = {
46
+ key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
47
+ cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
48
+ };
49
+ this.logger.info('Default certificates loaded successfully');
50
+ } catch (error) {
51
+ this.logger.error('Error loading default certificates', error);
52
+
53
+ // Generate self-signed fallback certificates
54
+ try {
55
+ // This is a placeholder for actual certificate generation code
56
+ // In a real implementation, you would use a library like selfsigned to generate certs
57
+ this.defaultCertificates = {
58
+ key: "FALLBACK_KEY_CONTENT",
59
+ cert: "FALLBACK_CERT_CONTENT"
60
+ };
61
+ this.logger.warn('Using fallback self-signed certificates');
62
+ } catch (fallbackError) {
63
+ this.logger.error('Failed to generate fallback certificates', fallbackError);
64
+ throw new Error('Could not load or generate SSL certificates');
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Set the HTTPS server reference for context updates
71
+ */
72
+ public setHttpsServer(server: plugins.https.Server): void {
73
+ this.httpsServer = server;
74
+ }
75
+
76
+ /**
77
+ * Get default certificates
78
+ */
79
+ public getDefaultCertificates(): { key: string; cert: string } {
80
+ return { ...this.defaultCertificates };
81
+ }
82
+
83
+ /**
84
+ * Sets an external Port80Handler for certificate management
85
+ */
86
+ public setExternalPort80Handler(handler: Port80Handler): void {
87
+ if (this.port80Handler && !this.externalPort80Handler) {
88
+ this.logger.warn('Replacing existing internal Port80Handler with external handler');
89
+
90
+ // Clean up existing handler if needed
91
+ if (this.port80Handler !== handler) {
92
+ // Unregister event handlers to avoid memory leaks
93
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_ISSUED);
94
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_RENEWED);
95
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_FAILED);
96
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_EXPIRING);
97
+ }
98
+ }
99
+
100
+ // Set the external handler
101
+ this.port80Handler = handler;
102
+ this.externalPort80Handler = true;
103
+
104
+ // Register event handlers
105
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
106
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
107
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
108
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
109
+ this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
110
+ });
111
+
112
+ this.logger.info('External Port80Handler connected to CertificateManager');
113
+
114
+ // Register domains with Port80Handler if we have any certificates cached
115
+ if (this.certificateCache.size > 0) {
116
+ const domains = Array.from(this.certificateCache.keys())
117
+ .filter(domain => !domain.includes('*')); // Skip wildcard domains
118
+
119
+ this.registerDomainsWithPort80Handler(domains);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Handle newly issued or renewed certificates from Port80Handler
125
+ */
126
+ private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
127
+ const { domain, certificate, privateKey, expiryDate } = data;
128
+
129
+ this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
130
+
131
+ // Update certificate in HTTPS server
132
+ this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
133
+
134
+ // Save the certificate to the filesystem if not using external handler
135
+ if (!this.externalPort80Handler && this.options.acme?.certificateStore) {
136
+ this.saveCertificateToStore(domain, certificate, privateKey);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Handle certificate issuance failures
142
+ */
143
+ private handleCertificateFailed(data: { domain: string; error: string }): void {
144
+ this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`);
145
+ }
146
+
147
+ /**
148
+ * Saves certificate and private key to the filesystem
149
+ */
150
+ private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
151
+ try {
152
+ const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
153
+ const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
154
+
155
+ fs.writeFileSync(certPath, certificate);
156
+ fs.writeFileSync(keyPath, privateKey);
157
+
158
+ // Ensure private key has restricted permissions
159
+ try {
160
+ fs.chmodSync(keyPath, 0o600);
161
+ } catch (error) {
162
+ this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`);
163
+ }
164
+
165
+ this.logger.info(`Saved certificate for ${domain} to ${certPath}`);
166
+ } catch (error) {
167
+ this.logger.error(`Failed to save certificate for ${domain}: ${error}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handles SNI (Server Name Indication) for TLS connections
173
+ * Used by the HTTPS server to select the correct certificate for each domain
174
+ */
175
+ public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
176
+ this.logger.debug(`SNI request for domain: ${domain}`);
177
+
178
+ // Check if we have a certificate for this domain
179
+ const certs = this.certificateCache.get(domain);
180
+
181
+ if (certs) {
182
+ try {
183
+ // Create TLS context with the cached certificate
184
+ const context = plugins.tls.createSecureContext({
185
+ key: certs.key,
186
+ cert: certs.cert
187
+ });
188
+
189
+ this.logger.debug(`Using cached certificate for ${domain}`);
190
+ cb(null, context);
191
+ return;
192
+ } catch (err) {
193
+ this.logger.error(`Error creating secure context for ${domain}:`, err);
194
+ }
195
+ }
196
+
197
+ // Check if we should trigger certificate issuance
198
+ if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
199
+ // Check if this domain is already registered
200
+ const certData = this.port80Handler.getCertificate(domain);
201
+
202
+ if (!certData) {
203
+ this.logger.info(`No certificate found for ${domain}, registering for issuance`);
204
+
205
+ // Register with new domain options format
206
+ const domainOptions: IDomainOptions = {
207
+ domainName: domain,
208
+ sslRedirect: true,
209
+ acmeMaintenance: true
210
+ };
211
+
212
+ this.port80Handler.addDomain(domainOptions);
213
+ }
214
+ }
215
+
216
+ // Fall back to default certificate
217
+ try {
218
+ const context = plugins.tls.createSecureContext({
219
+ key: this.defaultCertificates.key,
220
+ cert: this.defaultCertificates.cert
221
+ });
222
+
223
+ this.logger.debug(`Using default certificate for ${domain}`);
224
+ cb(null, context);
225
+ } catch (err) {
226
+ this.logger.error(`Error creating default secure context:`, err);
227
+ cb(new Error('Cannot create secure context'), null);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Updates certificate in cache
233
+ */
234
+ public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
235
+ // Update certificate context in HTTPS server if it's running
236
+ if (this.httpsServer) {
237
+ try {
238
+ this.httpsServer.addContext(domain, {
239
+ key: privateKey,
240
+ cert: certificate
241
+ });
242
+ this.logger.debug(`Updated SSL context for domain: ${domain}`);
243
+ } catch (error) {
244
+ this.logger.error(`Error updating SSL context for domain ${domain}:`, error);
245
+ }
246
+ }
247
+
248
+ // Update certificate in cache
249
+ this.certificateCache.set(domain, {
250
+ key: privateKey,
251
+ cert: certificate,
252
+ expires: expiryDate
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Gets a certificate for a domain
258
+ */
259
+ public getCertificate(domain: string): ICertificateEntry | undefined {
260
+ return this.certificateCache.get(domain);
261
+ }
262
+
263
+ /**
264
+ * Requests a new certificate for a domain
265
+ */
266
+ public async requestCertificate(domain: string): Promise<boolean> {
267
+ if (!this.options.acme?.enabled && !this.externalPort80Handler) {
268
+ this.logger.warn('ACME certificate management is not enabled');
269
+ return false;
270
+ }
271
+
272
+ if (!this.port80Handler) {
273
+ this.logger.error('Port80Handler is not initialized');
274
+ return false;
275
+ }
276
+
277
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
278
+ if (domain.includes('*')) {
279
+ this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`);
280
+ return false;
281
+ }
282
+
283
+ try {
284
+ // Use the new domain options format
285
+ const domainOptions: IDomainOptions = {
286
+ domainName: domain,
287
+ sslRedirect: true,
288
+ acmeMaintenance: true
289
+ };
290
+
291
+ this.port80Handler.addDomain(domainOptions);
292
+ this.logger.info(`Certificate request submitted for domain: ${domain}`);
293
+ return true;
294
+ } catch (error) {
295
+ this.logger.error(`Error requesting certificate for domain ${domain}:`, error);
296
+ return false;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Registers domains with Port80Handler for ACME certificate management
302
+ */
303
+ public registerDomainsWithPort80Handler(domains: string[]): void {
304
+ if (!this.port80Handler) {
305
+ this.logger.warn('Port80Handler is not initialized');
306
+ return;
307
+ }
308
+
309
+ for (const domain of domains) {
310
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
311
+ if (domain.includes('*')) {
312
+ this.logger.info(`Skipping wildcard domain for ACME: ${domain}`);
313
+ continue;
314
+ }
315
+
316
+ // Skip domains already with certificates if configured to do so
317
+ if (this.options.acme?.skipConfiguredCerts) {
318
+ const cachedCert = this.certificateCache.get(domain);
319
+ if (cachedCert) {
320
+ this.logger.info(`Skipping domain with existing certificate: ${domain}`);
321
+ continue;
322
+ }
323
+ }
324
+
325
+ // Register the domain for certificate issuance with new domain options format
326
+ const domainOptions: IDomainOptions = {
327
+ domainName: domain,
328
+ sslRedirect: true,
329
+ acmeMaintenance: true
330
+ };
331
+
332
+ this.port80Handler.addDomain(domainOptions);
333
+ this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Initialize internal Port80Handler
339
+ */
340
+ public async initializePort80Handler(): Promise<Port80Handler | null> {
341
+ // Skip if using external handler
342
+ if (this.externalPort80Handler) {
343
+ this.logger.info('Using external Port80Handler, skipping initialization');
344
+ return this.port80Handler;
345
+ }
346
+
347
+ if (!this.options.acme?.enabled) {
348
+ return null;
349
+ }
350
+
351
+ // Create certificate manager
352
+ this.port80Handler = new Port80Handler({
353
+ port: this.options.acme.port,
354
+ contactEmail: this.options.acme.contactEmail,
355
+ useProduction: this.options.acme.useProduction,
356
+ renewThresholdDays: this.options.acme.renewThresholdDays,
357
+ httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
358
+ renewCheckIntervalHours: 24, // Check daily for renewals
359
+ enabled: this.options.acme.enabled,
360
+ autoRenew: this.options.acme.autoRenew,
361
+ certificateStore: this.options.acme.certificateStore,
362
+ skipConfiguredCerts: this.options.acme.skipConfiguredCerts
363
+ });
364
+
365
+ // Register event handlers
366
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
367
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
368
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
369
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
370
+ this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
371
+ });
372
+
373
+ // Start the handler
374
+ try {
375
+ await this.port80Handler.start();
376
+ this.logger.info(`Port80Handler started on port ${this.options.acme.port}`);
377
+ return this.port80Handler;
378
+ } catch (error) {
379
+ this.logger.error(`Failed to start Port80Handler: ${error}`);
380
+ this.port80Handler = null;
381
+ return null;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Stop the Port80Handler if it was internally created
387
+ */
388
+ public async stopPort80Handler(): Promise<void> {
389
+ if (this.port80Handler && !this.externalPort80Handler) {
390
+ try {
391
+ await this.port80Handler.stop();
392
+ this.logger.info('Port80Handler stopped');
393
+ } catch (error) {
394
+ this.logger.error('Error stopping Port80Handler', error);
395
+ }
396
+ }
397
+ }
398
+ }
@@ -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
+ }