@push.rocks/smartproxy 3.34.0 → 3.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { NetworkProxy } from './classes.networkproxy.js';
3
+ import { SniHandler } from './classes.snihandler.js';
3
4
 
4
5
  /** Domain configuration with per-domain allowed port ranges */
5
6
  export interface IDomainConfig {
@@ -56,9 +57,21 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
56
57
  keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
57
58
  extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
58
59
 
59
- // New property for NetworkProxy integration
60
+ // NetworkProxy integration
60
61
  useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
61
62
  networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
63
+
64
+ // ACME certificate management options
65
+ acme?: {
66
+ enabled?: boolean; // Whether to enable automatic certificate management
67
+ port?: number; // Port to listen on for ACME challenges (default: 80)
68
+ contactEmail?: string; // Email for Let's Encrypt account
69
+ useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
70
+ renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
71
+ autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
72
+ certificateStore?: string; // Directory to store certificates (default: ./certs)
73
+ skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
74
+ };
62
75
  }
63
76
 
64
77
  /**
@@ -105,192 +118,8 @@ interface IConnectionRecord {
105
118
  domainSwitches?: number; // Number of times the domain has been switched on this connection
106
119
  }
107
120
 
108
- /**
109
- * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
110
- * Enhanced for robustness and detailed logging.
111
- * @param buffer - Buffer containing the TLS ClientHello.
112
- * @param enableLogging - Whether to enable detailed logging.
113
- * @returns The server name if found, otherwise undefined.
114
- */
115
- function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
116
- try {
117
- // Check if buffer is too small for TLS
118
- if (buffer.length < 5) {
119
- if (enableLogging) console.log('Buffer too small for TLS header');
120
- return undefined;
121
- }
122
-
123
- // Check record type (has to be handshake - 22)
124
- const recordType = buffer.readUInt8(0);
125
- if (recordType !== 22) {
126
- if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
127
- return undefined;
128
- }
129
-
130
- // Check TLS version (has to be 3.1 or higher)
131
- const majorVersion = buffer.readUInt8(1);
132
- const minorVersion = buffer.readUInt8(2);
133
- if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
134
-
135
- // Check record length
136
- const recordLength = buffer.readUInt16BE(3);
137
- if (buffer.length < 5 + recordLength) {
138
- if (enableLogging)
139
- console.log(
140
- `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
141
- );
142
- return undefined;
143
- }
144
-
145
- let offset = 5;
146
- const handshakeType = buffer.readUInt8(offset);
147
- if (handshakeType !== 1) {
148
- if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
149
- return undefined;
150
- }
151
-
152
- offset += 4; // Skip handshake header (type + length)
153
-
154
- // Client version
155
- const clientMajorVersion = buffer.readUInt8(offset);
156
- const clientMinorVersion = buffer.readUInt8(offset + 1);
157
- if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
158
-
159
- offset += 2 + 32; // Skip client version and random
160
-
161
- // Session ID
162
- const sessionIDLength = buffer.readUInt8(offset);
163
- if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
164
- offset += 1 + sessionIDLength; // Skip session ID
165
-
166
- // Cipher suites
167
- if (offset + 2 > buffer.length) {
168
- if (enableLogging) console.log('Buffer too small for cipher suites length');
169
- return undefined;
170
- }
171
- const cipherSuitesLength = buffer.readUInt16BE(offset);
172
- if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
173
- offset += 2 + cipherSuitesLength; // Skip cipher suites
174
-
175
- // Compression methods
176
- if (offset + 1 > buffer.length) {
177
- if (enableLogging) console.log('Buffer too small for compression methods length');
178
- return undefined;
179
- }
180
- const compressionMethodsLength = buffer.readUInt8(offset);
181
- if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
182
- offset += 1 + compressionMethodsLength; // Skip compression methods
183
-
184
- // Extensions
185
- if (offset + 2 > buffer.length) {
186
- if (enableLogging) console.log('Buffer too small for extensions length');
187
- return undefined;
188
- }
189
- const extensionsLength = buffer.readUInt16BE(offset);
190
- if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
191
- offset += 2;
192
- const extensionsEnd = offset + extensionsLength;
193
-
194
- if (extensionsEnd > buffer.length) {
195
- if (enableLogging)
196
- console.log(
197
- `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
198
- );
199
- return undefined;
200
- }
201
-
202
- // Parse extensions
203
- while (offset + 4 <= extensionsEnd) {
204
- const extensionType = buffer.readUInt16BE(offset);
205
- const extensionLength = buffer.readUInt16BE(offset + 2);
206
-
207
- if (enableLogging)
208
- console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
209
-
210
- offset += 4;
211
-
212
- if (extensionType === 0x0000) {
213
- // SNI extension
214
- if (offset + 2 > buffer.length) {
215
- if (enableLogging) console.log('Buffer too small for SNI list length');
216
- return undefined;
217
- }
218
-
219
- const sniListLength = buffer.readUInt16BE(offset);
220
- if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
221
- offset += 2;
222
- const sniListEnd = offset + sniListLength;
223
-
224
- if (sniListEnd > buffer.length) {
225
- if (enableLogging)
226
- console.log(
227
- `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
228
- );
229
- return undefined;
230
- }
231
-
232
- while (offset + 3 < sniListEnd) {
233
- const nameType = buffer.readUInt8(offset++);
234
- const nameLen = buffer.readUInt16BE(offset);
235
- offset += 2;
236
-
237
- if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
238
-
239
- if (nameType === 0) {
240
- // host_name
241
- if (offset + nameLen > buffer.length) {
242
- if (enableLogging)
243
- console.log(
244
- `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
245
- buffer.length
246
- }`
247
- );
248
- return undefined;
249
- }
250
-
251
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
252
- if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
253
- return serverName;
254
- }
255
-
256
- offset += nameLen;
257
- }
258
- break;
259
- } else {
260
- offset += extensionLength;
261
- }
262
- }
263
-
264
- if (enableLogging) console.log('No SNI extension found');
265
- return undefined;
266
- } catch (err) {
267
- console.log(`Error extracting SNI: ${err}`);
268
- return undefined;
269
- }
270
- }
271
-
272
- /**
273
- * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
274
- * @param buffer - Buffer containing the TLS record
275
- * @returns true if the buffer contains a proper ClientHello message
276
- */
277
- function isClientHello(buffer: Buffer): boolean {
278
- try {
279
- if (buffer.length < 9) return false; // Too small for a proper ClientHello
280
-
281
- // Check record type (has to be handshake - 22)
282
- if (buffer.readUInt8(0) !== 22) return false;
283
-
284
- // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
285
- if (buffer.readUInt8(5) !== 1) return false;
286
-
287
- // Basic checks passed, this appears to be a ClientHello
288
- return true;
289
- } catch (err) {
290
- console.log(`Error checking for ClientHello: ${err}`);
291
- return false;
292
- }
293
- }
121
+ // SNI functions are now imported from SniHandler class
122
+ // No need for wrapper functions
294
123
 
295
124
  // Helper: Check if a port falls within any of the given port ranges
296
125
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
@@ -334,10 +163,7 @@ const generateConnectionId = (): string => {
334
163
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
335
164
  };
336
165
 
337
- // Helper: Check if a buffer contains a TLS handshake
338
- const isTlsHandshake = (buffer: Buffer): boolean => {
339
- return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
340
- };
166
+ // SNI functions are now imported from SniHandler class
341
167
 
342
168
  // Helper: Ensure timeout values don't exceed Node.js max safe integer
343
169
  const ensureSafeTimeout = (timeout: number): number => {
@@ -418,6 +244,18 @@ export class PortProxy {
418
244
 
419
245
  // NetworkProxy settings
420
246
  networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
247
+
248
+ // ACME certificate settings with reasonable defaults
249
+ acme: settingsArg.acme || {
250
+ enabled: false,
251
+ port: 80,
252
+ contactEmail: 'admin@example.com',
253
+ useProduction: false,
254
+ renewThresholdDays: 30,
255
+ autoRenew: true,
256
+ certificateStore: './certs',
257
+ skipConfiguredCerts: false
258
+ }
421
259
  };
422
260
 
423
261
  // Initialize NetworkProxy if enabled
@@ -429,15 +267,182 @@ export class PortProxy {
429
267
  /**
430
268
  * Initialize NetworkProxy instance
431
269
  */
432
- private initializeNetworkProxy(): void {
270
+ private async initializeNetworkProxy(): Promise<void> {
433
271
  if (!this.networkProxy) {
434
- this.networkProxy = new NetworkProxy({
272
+ // Configure NetworkProxy options based on PortProxy settings
273
+ const networkProxyOptions: any = {
435
274
  port: this.settings.networkProxyPort!,
436
275
  portProxyIntegration: true,
437
276
  logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
438
- });
277
+ };
278
+
279
+ // Add ACME settings if configured
280
+ if (this.settings.acme) {
281
+ networkProxyOptions.acme = { ...this.settings.acme };
282
+ }
283
+
284
+ this.networkProxy = new NetworkProxy(networkProxyOptions);
439
285
 
440
286
  console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
287
+
288
+ // Convert and apply domain configurations to NetworkProxy
289
+ await this.syncDomainConfigsToNetworkProxy();
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Updates the domain configurations for the proxy
295
+ * @param newDomainConfigs The new domain configurations
296
+ */
297
+ public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
298
+ console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
299
+ this.settings.domainConfigs = newDomainConfigs;
300
+
301
+ // If NetworkProxy is initialized, resync the configurations
302
+ if (this.networkProxy) {
303
+ await this.syncDomainConfigsToNetworkProxy();
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Updates the ACME certificate settings
309
+ * @param acmeSettings New ACME settings
310
+ */
311
+ public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
312
+ console.log('Updating ACME certificate settings');
313
+
314
+ // Update settings
315
+ this.settings.acme = {
316
+ ...this.settings.acme,
317
+ ...acmeSettings
318
+ };
319
+
320
+ // If NetworkProxy is initialized, update its ACME settings
321
+ if (this.networkProxy) {
322
+ try {
323
+ // Recreate NetworkProxy with new settings if ACME enabled state has changed
324
+ if (this.settings.acme.enabled !== acmeSettings.enabled) {
325
+ console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
326
+
327
+ // Stop the current NetworkProxy
328
+ await this.networkProxy.stop();
329
+ this.networkProxy = null;
330
+
331
+ // Reinitialize with new settings
332
+ await this.initializeNetworkProxy();
333
+
334
+ // Use start() to make sure ACME gets initialized if newly enabled
335
+ await this.networkProxy.start();
336
+ } else {
337
+ // Update existing NetworkProxy with new settings
338
+ // Note: Some settings may require a restart to take effect
339
+ console.log('Updating ACME settings in NetworkProxy');
340
+
341
+ // For certificate renewals, we might want to trigger checks with the new settings
342
+ if (acmeSettings.renewThresholdDays) {
343
+ console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
344
+ // This is implementation-dependent but gives an example
345
+ if (this.networkProxy.options.acme) {
346
+ this.networkProxy.options.acme.renewThresholdDays = acmeSettings.renewThresholdDays;
347
+ }
348
+ }
349
+ }
350
+ } catch (err) {
351
+ console.log(`Error updating ACME settings: ${err}`);
352
+ }
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Synchronizes PortProxy domain configurations to NetworkProxy
358
+ * This allows domains configured in PortProxy to be used by NetworkProxy
359
+ */
360
+ private async syncDomainConfigsToNetworkProxy(): Promise<void> {
361
+ if (!this.networkProxy) {
362
+ console.log('Cannot sync configurations - NetworkProxy not initialized');
363
+ return;
364
+ }
365
+
366
+ try {
367
+ // Get SSL certificates from assets
368
+ // Import fs directly since it's not in plugins
369
+ const fs = await import('fs');
370
+
371
+ let certPair;
372
+ try {
373
+ certPair = {
374
+ key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
375
+ cert: fs.readFileSync('assets/certs/cert.pem', 'utf8')
376
+ };
377
+ } catch (certError) {
378
+ console.log(`Warning: Could not read default certificates: ${certError}`);
379
+ console.log('Using empty certificate placeholders - ACME will generate proper certificates if enabled');
380
+
381
+ // Use empty placeholders - NetworkProxy will use its internal defaults
382
+ // or ACME will generate proper ones if enabled
383
+ certPair = {
384
+ key: '',
385
+ cert: ''
386
+ };
387
+ }
388
+
389
+ // Convert domain configs to NetworkProxy configs
390
+ const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
391
+ this.settings.domainConfigs,
392
+ certPair
393
+ );
394
+
395
+ // Log ACME-eligible domains if ACME is enabled
396
+ if (this.settings.acme?.enabled) {
397
+ const acmeEligibleDomains = proxyConfigs
398
+ .filter(config => !config.hostName.includes('*')) // Exclude wildcards
399
+ .map(config => config.hostName);
400
+
401
+ if (acmeEligibleDomains.length > 0) {
402
+ console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
403
+ } else {
404
+ console.log('No domains eligible for ACME certificates found in configuration');
405
+ }
406
+ }
407
+
408
+ // Update NetworkProxy with the converted configs
409
+ this.networkProxy.updateProxyConfigs(proxyConfigs).then(() => {
410
+ console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`);
411
+ }).catch(err => {
412
+ console.log(`Error synchronizing configurations: ${err.message}`);
413
+ });
414
+ } catch (err) {
415
+ console.log(`Failed to sync configurations: ${err}`);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Requests a certificate for a specific domain
421
+ * @param domain The domain to request a certificate for
422
+ * @returns Promise that resolves to true if the request was successful, false otherwise
423
+ */
424
+ public async requestCertificate(domain: string): Promise<boolean> {
425
+ if (!this.networkProxy) {
426
+ console.log('Cannot request certificate - NetworkProxy not initialized');
427
+ return false;
428
+ }
429
+
430
+ if (!this.settings.acme?.enabled) {
431
+ console.log('Cannot request certificate - ACME is not enabled');
432
+ return false;
433
+ }
434
+
435
+ try {
436
+ const result = await this.networkProxy.requestCertificate(domain);
437
+ if (result) {
438
+ console.log(`Certificate request for ${domain} submitted successfully`);
439
+ } else {
440
+ console.log(`Certificate request for ${domain} failed`);
441
+ }
442
+ return result;
443
+ } catch (err) {
444
+ console.log(`Error requesting certificate: ${err}`);
445
+ return false;
441
446
  }
442
447
  }
443
448
 
@@ -570,7 +575,7 @@ export class PortProxy {
570
575
  record.bytesReceived += chunk.length;
571
576
 
572
577
  // Check for TLS handshake
573
- if (!record.isTLS && isTlsHandshake(chunk)) {
578
+ if (!record.isTLS && SniHandler.isTlsHandshake(chunk)) {
574
579
  record.isTLS = true;
575
580
 
576
581
  if (this.settings.enableTlsDebugLogging) {
@@ -858,10 +863,10 @@ export class PortProxy {
858
863
  // Define a handler for checking renegotiation with improved detection
859
864
  const renegotiationHandler = (renegChunk: Buffer) => {
860
865
  // Only process if this looks like a TLS ClientHello
861
- if (isClientHello(renegChunk)) {
866
+ if (SniHandler.isClientHello(renegChunk)) {
862
867
  try {
863
868
  // Extract SNI from ClientHello
864
- const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
869
+ const newSNI = SniHandler.extractSNIWithResumptionSupport(renegChunk, this.settings.enableTlsDebugLogging);
865
870
 
866
871
  // Skip if no SNI was found
867
872
  if (!newSNI) return;
@@ -1278,10 +1283,27 @@ export class PortProxy {
1278
1283
  return;
1279
1284
  }
1280
1285
 
1286
+ // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
1287
+ if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0 && !this.networkProxy) {
1288
+ await this.initializeNetworkProxy();
1289
+ }
1290
+
1281
1291
  // Start NetworkProxy if configured
1282
1292
  if (this.networkProxy) {
1283
1293
  await this.networkProxy.start();
1284
1294
  console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
1295
+
1296
+ // Log ACME status
1297
+ if (this.settings.acme?.enabled) {
1298
+ console.log(`ACME certificate management is enabled (${this.settings.acme.useProduction ? 'Production' : 'Staging'} mode)`);
1299
+ console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
1300
+
1301
+ // Register domains for ACME certificates if enabled
1302
+ if (this.networkProxy.options.acme?.enabled) {
1303
+ console.log('Registering domains with ACME certificate manager...');
1304
+ // The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
1305
+ }
1306
+ }
1285
1307
  }
1286
1308
 
1287
1309
  // Define a unified connection handler for all listening ports.
@@ -1436,7 +1458,7 @@ export class PortProxy {
1436
1458
  connectionRecord.hasReceivedInitialData = true;
1437
1459
 
1438
1460
  // Check if this looks like a TLS handshake
1439
- if (isTlsHandshake(chunk)) {
1461
+ if (SniHandler.isTlsHandshake(chunk)) {
1440
1462
  connectionRecord.isTLS = true;
1441
1463
 
1442
1464
  // Forward directly to NetworkProxy without SNI processing
@@ -1498,7 +1520,7 @@ export class PortProxy {
1498
1520
  this.updateActivity(connectionRecord);
1499
1521
 
1500
1522
  // Check for TLS handshake if this is the first chunk
1501
- if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
1523
+ if (!connectionRecord.isTLS && SniHandler.isTlsHandshake(chunk)) {
1502
1524
  connectionRecord.isTLS = true;
1503
1525
 
1504
1526
  if (this.settings.enableTlsDebugLogging) {
@@ -1506,7 +1528,7 @@ export class PortProxy {
1506
1528
  `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1507
1529
  );
1508
1530
  // Try to extract SNI and log detailed debug info
1509
- extractSNI(chunk, true);
1531
+ SniHandler.extractSNIWithResumptionSupport(chunk, true);
1510
1532
  }
1511
1533
  }
1512
1534
  });
@@ -1535,7 +1557,7 @@ export class PortProxy {
1535
1557
  connectionRecord.hasReceivedInitialData = true;
1536
1558
 
1537
1559
  // Check if this looks like a TLS handshake
1538
- const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
1560
+ const isTlsHandshakeDetected = initialChunk && SniHandler.isTlsHandshake(initialChunk);
1539
1561
  if (isTlsHandshakeDetected) {
1540
1562
  connectionRecord.isTLS = true;
1541
1563
 
@@ -1704,7 +1726,7 @@ export class PortProxy {
1704
1726
  // Try to extract SNI
1705
1727
  let serverName = '';
1706
1728
 
1707
- if (isTlsHandshake(chunk)) {
1729
+ if (SniHandler.isTlsHandshake(chunk)) {
1708
1730
  connectionRecord.isTLS = true;
1709
1731
 
1710
1732
  if (this.settings.enableTlsDebugLogging) {
@@ -1713,7 +1735,7 @@ export class PortProxy {
1713
1735
  );
1714
1736
  }
1715
1737
 
1716
- serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1738
+ serverName = SniHandler.extractSNIWithResumptionSupport(chunk, this.settings.enableTlsDebugLogging) || '';
1717
1739
  }
1718
1740
 
1719
1741
  // Lock the connection to the negotiated SNI.
@@ -2036,11 +2058,17 @@ export class PortProxy {
2036
2058
  }
2037
2059
  }
2038
2060
 
2039
- // Stop NetworkProxy if it was started
2061
+ // Stop NetworkProxy if it was started (which also stops ACME manager)
2040
2062
  if (this.networkProxy) {
2041
2063
  try {
2064
+ console.log('Stopping NetworkProxy...');
2042
2065
  await this.networkProxy.stop();
2043
2066
  console.log('NetworkProxy stopped successfully');
2067
+
2068
+ // Log ACME shutdown if it was enabled
2069
+ if (this.settings.acme?.enabled) {
2070
+ console.log('ACME certificate manager stopped');
2071
+ }
2044
2072
  } catch (err) {
2045
2073
  console.log(`Error stopping NetworkProxy: ${err}`);
2046
2074
  }