@push.rocks/smartproxy 3.41.6 → 3.41.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ts/classes.portproxy.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface IDomainConfig {
|
|
|
11
11
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
12
12
|
// Allow domain-specific timeout override
|
|
13
13
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
// NetworkProxy integration options for this specific domain
|
|
16
16
|
useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain
|
|
17
17
|
networkProxyPort?: number; // Override default NetworkProxy port for this domain
|
|
@@ -65,17 +65,17 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
65
65
|
// NetworkProxy integration
|
|
66
66
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
67
67
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
// ACME certificate management options
|
|
70
70
|
acme?: {
|
|
71
|
-
enabled?: boolean;
|
|
72
|
-
port?: number;
|
|
73
|
-
contactEmail?: string;
|
|
74
|
-
useProduction?: boolean;
|
|
75
|
-
renewThresholdDays?: number;
|
|
76
|
-
autoRenew?: boolean;
|
|
77
|
-
certificateStore?: string;
|
|
78
|
-
skipConfiguredCerts?: boolean;
|
|
71
|
+
enabled?: boolean; // Whether to enable automatic certificate management
|
|
72
|
+
port?: number; // Port to listen on for ACME challenges (default: 80)
|
|
73
|
+
contactEmail?: string; // Email for Let's Encrypt account
|
|
74
|
+
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
|
75
|
+
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
|
76
|
+
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
|
77
|
+
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
|
78
|
+
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -232,13 +232,13 @@ export class PortProxy {
|
|
|
232
232
|
|
|
233
233
|
// Feature flags
|
|
234
234
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
235
|
-
enableKeepAliveProbes:
|
|
236
|
-
|
|
235
|
+
enableKeepAliveProbes:
|
|
236
|
+
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
|
237
237
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
238
238
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
239
239
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
240
|
-
allowSessionTicket:
|
|
241
|
-
|
|
240
|
+
allowSessionTicket:
|
|
241
|
+
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
|
|
242
242
|
|
|
243
243
|
// Rate limiting defaults
|
|
244
244
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
|
@@ -248,10 +248,10 @@ export class PortProxy {
|
|
|
248
248
|
keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
|
|
249
249
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
|
|
250
250
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
// NetworkProxy settings
|
|
253
253
|
networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
// ACME certificate settings with reasonable defaults
|
|
256
256
|
acme: settingsArg.acme || {
|
|
257
257
|
enabled: false,
|
|
@@ -261,8 +261,8 @@ export class PortProxy {
|
|
|
261
261
|
renewThresholdDays: 30,
|
|
262
262
|
autoRenew: true,
|
|
263
263
|
certificateStore: './certs',
|
|
264
|
-
skipConfiguredCerts: false
|
|
265
|
-
}
|
|
264
|
+
skipConfiguredCerts: false,
|
|
265
|
+
},
|
|
266
266
|
};
|
|
267
267
|
|
|
268
268
|
// Initialize NetworkProxy if enabled
|
|
@@ -280,23 +280,23 @@ export class PortProxy {
|
|
|
280
280
|
const networkProxyOptions: any = {
|
|
281
281
|
port: this.settings.networkProxyPort!,
|
|
282
282
|
portProxyIntegration: true,
|
|
283
|
-
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
|
|
283
|
+
logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
|
|
284
284
|
};
|
|
285
|
-
|
|
285
|
+
|
|
286
286
|
// Add ACME settings if configured
|
|
287
287
|
if (this.settings.acme) {
|
|
288
288
|
networkProxyOptions.acme = { ...this.settings.acme };
|
|
289
289
|
}
|
|
290
|
-
|
|
290
|
+
|
|
291
291
|
this.networkProxy = new NetworkProxy(networkProxyOptions);
|
|
292
|
-
|
|
292
|
+
|
|
293
293
|
console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
// Convert and apply domain configurations to NetworkProxy
|
|
296
296
|
await this.syncDomainConfigsToNetworkProxy();
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
/**
|
|
301
301
|
* Updates the domain configurations for the proxy
|
|
302
302
|
* @param newDomainConfigs The new domain configurations
|
|
@@ -304,47 +304,47 @@ export class PortProxy {
|
|
|
304
304
|
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
|
|
305
305
|
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
|
306
306
|
this.settings.domainConfigs = newDomainConfigs;
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
// If NetworkProxy is initialized, resync the configurations
|
|
309
309
|
if (this.networkProxy) {
|
|
310
310
|
await this.syncDomainConfigsToNetworkProxy();
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
/**
|
|
315
315
|
* Updates the ACME certificate settings
|
|
316
316
|
* @param acmeSettings New ACME settings
|
|
317
317
|
*/
|
|
318
318
|
public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
|
|
319
319
|
console.log('Updating ACME certificate settings');
|
|
320
|
-
|
|
320
|
+
|
|
321
321
|
// Update settings
|
|
322
322
|
this.settings.acme = {
|
|
323
323
|
...this.settings.acme,
|
|
324
|
-
...acmeSettings
|
|
324
|
+
...acmeSettings,
|
|
325
325
|
};
|
|
326
|
-
|
|
326
|
+
|
|
327
327
|
// If NetworkProxy is initialized, update its ACME settings
|
|
328
328
|
if (this.networkProxy) {
|
|
329
329
|
try {
|
|
330
330
|
// Recreate NetworkProxy with new settings if ACME enabled state has changed
|
|
331
331
|
if (this.settings.acme.enabled !== acmeSettings.enabled) {
|
|
332
332
|
console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
|
|
333
|
-
|
|
333
|
+
|
|
334
334
|
// Stop the current NetworkProxy
|
|
335
335
|
await this.networkProxy.stop();
|
|
336
336
|
this.networkProxy = null;
|
|
337
|
-
|
|
337
|
+
|
|
338
338
|
// Reinitialize with new settings
|
|
339
339
|
await this.initializeNetworkProxy();
|
|
340
|
-
|
|
340
|
+
|
|
341
341
|
// Use start() to make sure ACME gets initialized if newly enabled
|
|
342
342
|
await this.networkProxy.start();
|
|
343
343
|
} else {
|
|
344
344
|
// Update existing NetworkProxy with new settings
|
|
345
345
|
// Note: Some settings may require a restart to take effect
|
|
346
346
|
console.log('Updating ACME settings in NetworkProxy');
|
|
347
|
-
|
|
347
|
+
|
|
348
348
|
// For certificate renewals, we might want to trigger checks with the new settings
|
|
349
349
|
if (acmeSettings.renewThresholdDays) {
|
|
350
350
|
console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
|
|
@@ -359,7 +359,7 @@ export class PortProxy {
|
|
|
359
359
|
}
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
|
-
|
|
362
|
+
|
|
363
363
|
/**
|
|
364
364
|
* Synchronizes PortProxy domain configurations to NetworkProxy
|
|
365
365
|
* This allows domains configured in PortProxy to be used by NetworkProxy
|
|
@@ -369,60 +369,67 @@ export class PortProxy {
|
|
|
369
369
|
console.log('Cannot sync configurations - NetworkProxy not initialized');
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
|
-
|
|
372
|
+
|
|
373
373
|
try {
|
|
374
374
|
// Get SSL certificates from assets
|
|
375
375
|
// Import fs directly since it's not in plugins
|
|
376
376
|
const fs = await import('fs');
|
|
377
|
-
|
|
377
|
+
|
|
378
378
|
let certPair;
|
|
379
379
|
try {
|
|
380
380
|
certPair = {
|
|
381
381
|
key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
|
|
382
|
-
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8')
|
|
382
|
+
cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
|
|
383
383
|
};
|
|
384
384
|
} catch (certError) {
|
|
385
385
|
console.log(`Warning: Could not read default certificates: ${certError}`);
|
|
386
|
-
console.log(
|
|
387
|
-
|
|
386
|
+
console.log(
|
|
387
|
+
'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
|
|
388
|
+
);
|
|
389
|
+
|
|
388
390
|
// Use empty placeholders - NetworkProxy will use its internal defaults
|
|
389
391
|
// or ACME will generate proper ones if enabled
|
|
390
392
|
certPair = {
|
|
391
393
|
key: '',
|
|
392
|
-
cert: ''
|
|
394
|
+
cert: '',
|
|
393
395
|
};
|
|
394
396
|
}
|
|
395
|
-
|
|
397
|
+
|
|
396
398
|
// Convert domain configs to NetworkProxy configs
|
|
397
399
|
const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
|
|
398
400
|
this.settings.domainConfigs,
|
|
399
401
|
certPair
|
|
400
402
|
);
|
|
401
|
-
|
|
403
|
+
|
|
402
404
|
// Log ACME-eligible domains if ACME is enabled
|
|
403
405
|
if (this.settings.acme?.enabled) {
|
|
404
406
|
const acmeEligibleDomains = proxyConfigs
|
|
405
|
-
.filter(config => !config.hostName.includes('*')) // Exclude wildcards
|
|
406
|
-
.map(config => config.hostName);
|
|
407
|
-
|
|
407
|
+
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
|
408
|
+
.map((config) => config.hostName);
|
|
409
|
+
|
|
408
410
|
if (acmeEligibleDomains.length > 0) {
|
|
409
411
|
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
|
|
410
412
|
} else {
|
|
411
413
|
console.log('No domains eligible for ACME certificates found in configuration');
|
|
412
414
|
}
|
|
413
415
|
}
|
|
414
|
-
|
|
416
|
+
|
|
415
417
|
// Update NetworkProxy with the converted configs
|
|
416
|
-
this.networkProxy
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
418
|
+
this.networkProxy
|
|
419
|
+
.updateProxyConfigs(proxyConfigs)
|
|
420
|
+
.then(() => {
|
|
421
|
+
console.log(
|
|
422
|
+
`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`
|
|
423
|
+
);
|
|
424
|
+
})
|
|
425
|
+
.catch((err) => {
|
|
426
|
+
console.log(`Error synchronizing configurations: ${err.message}`);
|
|
427
|
+
});
|
|
421
428
|
} catch (err) {
|
|
422
429
|
console.log(`Failed to sync configurations: ${err}`);
|
|
423
430
|
}
|
|
424
431
|
}
|
|
425
|
-
|
|
432
|
+
|
|
426
433
|
/**
|
|
427
434
|
* Requests a certificate for a specific domain
|
|
428
435
|
* @param domain The domain to request a certificate for
|
|
@@ -433,12 +440,12 @@ export class PortProxy {
|
|
|
433
440
|
console.log('Cannot request certificate - NetworkProxy not initialized');
|
|
434
441
|
return false;
|
|
435
442
|
}
|
|
436
|
-
|
|
443
|
+
|
|
437
444
|
if (!this.settings.acme?.enabled) {
|
|
438
445
|
console.log('Cannot request certificate - ACME is not enabled');
|
|
439
446
|
return false;
|
|
440
447
|
}
|
|
441
|
-
|
|
448
|
+
|
|
442
449
|
try {
|
|
443
450
|
const result = await this.networkProxy.requestCertificate(domain);
|
|
444
451
|
if (result) {
|
|
@@ -546,9 +553,7 @@ export class PortProxy {
|
|
|
546
553
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
547
554
|
|
|
548
555
|
if (this.settings.enableDetailedLogging) {
|
|
549
|
-
console.log(
|
|
550
|
-
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
|
|
551
|
-
);
|
|
556
|
+
console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
|
|
552
557
|
}
|
|
553
558
|
});
|
|
554
559
|
}
|
|
@@ -582,11 +587,11 @@ export class PortProxy {
|
|
|
582
587
|
let queueSize = 0;
|
|
583
588
|
let processingQueue = false;
|
|
584
589
|
let drainPending = false;
|
|
585
|
-
|
|
590
|
+
|
|
586
591
|
// Flag to track if we've switched to the final piping mechanism
|
|
587
592
|
// Once this is true, we no longer buffer data in dataQueue
|
|
588
593
|
let pipingEstablished = false;
|
|
589
|
-
|
|
594
|
+
|
|
590
595
|
// Pause the incoming socket to prevent buffer overflows
|
|
591
596
|
// This ensures we control the flow of data until piping is set up
|
|
592
597
|
socket.pause();
|
|
@@ -594,22 +599,22 @@ export class PortProxy {
|
|
|
594
599
|
// Function to safely process the data queue without losing events
|
|
595
600
|
const processDataQueue = () => {
|
|
596
601
|
if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
|
|
597
|
-
|
|
602
|
+
|
|
598
603
|
processingQueue = true;
|
|
599
|
-
|
|
604
|
+
|
|
600
605
|
try {
|
|
601
606
|
// Process all queued chunks with the current active handler
|
|
602
607
|
while (dataQueue.length > 0) {
|
|
603
608
|
const chunk = dataQueue.shift()!;
|
|
604
609
|
queueSize -= chunk.length;
|
|
605
|
-
|
|
610
|
+
|
|
606
611
|
// Once piping is established, we shouldn't get here,
|
|
607
612
|
// but just in case, pass to the outgoing socket directly
|
|
608
613
|
if (pipingEstablished && record.outgoing) {
|
|
609
614
|
record.outgoing.write(chunk);
|
|
610
615
|
continue;
|
|
611
616
|
}
|
|
612
|
-
|
|
617
|
+
|
|
613
618
|
// Track bytes received
|
|
614
619
|
record.bytesReceived += chunk.length;
|
|
615
620
|
|
|
@@ -643,7 +648,7 @@ export class PortProxy {
|
|
|
643
648
|
}
|
|
644
649
|
} finally {
|
|
645
650
|
processingQueue = false;
|
|
646
|
-
|
|
651
|
+
|
|
647
652
|
// If there's a pending drain and we've processed everything,
|
|
648
653
|
// signal we're ready for more data if we haven't established piping yet
|
|
649
654
|
if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
|
|
@@ -657,17 +662,17 @@ export class PortProxy {
|
|
|
657
662
|
const safeDataHandler = (chunk: Buffer) => {
|
|
658
663
|
// If piping is already established, just let the pipe handle it
|
|
659
664
|
if (pipingEstablished) return;
|
|
660
|
-
|
|
665
|
+
|
|
661
666
|
// Add to our queue for orderly processing
|
|
662
667
|
dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
|
|
663
668
|
queueSize += chunk.length;
|
|
664
|
-
|
|
669
|
+
|
|
665
670
|
// If queue is getting large, pause socket until we catch up
|
|
666
671
|
if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
|
|
667
672
|
socket.pause();
|
|
668
673
|
drainPending = true;
|
|
669
674
|
}
|
|
670
|
-
|
|
675
|
+
|
|
671
676
|
// Process the queue
|
|
672
677
|
processDataQueue();
|
|
673
678
|
};
|
|
@@ -848,19 +853,19 @@ export class PortProxy {
|
|
|
848
853
|
|
|
849
854
|
// Process any remaining data in the queue before switching to piping
|
|
850
855
|
processDataQueue();
|
|
851
|
-
|
|
856
|
+
|
|
852
857
|
// Setup function to establish piping - we'll use this after flushing data
|
|
853
858
|
const setupPiping = () => {
|
|
854
859
|
// Mark that we're switching to piping mode
|
|
855
860
|
pipingEstablished = true;
|
|
856
|
-
|
|
861
|
+
|
|
857
862
|
// Setup piping in both directions
|
|
858
863
|
socket.pipe(targetSocket);
|
|
859
864
|
targetSocket.pipe(socket);
|
|
860
|
-
|
|
865
|
+
|
|
861
866
|
// Resume the socket to ensure data flows
|
|
862
867
|
socket.resume();
|
|
863
|
-
|
|
868
|
+
|
|
864
869
|
// Process any data that might be queued in the interim
|
|
865
870
|
if (dataQueue.length > 0) {
|
|
866
871
|
// Write any remaining queued data directly to the target socket
|
|
@@ -871,7 +876,7 @@ export class PortProxy {
|
|
|
871
876
|
dataQueue.length = 0;
|
|
872
877
|
queueSize = 0;
|
|
873
878
|
}
|
|
874
|
-
|
|
879
|
+
|
|
875
880
|
if (this.settings.enableDetailedLogging) {
|
|
876
881
|
console.log(
|
|
877
882
|
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
@@ -935,30 +940,36 @@ export class PortProxy {
|
|
|
935
940
|
sourceIp: record.remoteIP,
|
|
936
941
|
sourcePort: record.incoming.remotePort || 0,
|
|
937
942
|
destIp: record.incoming.localAddress || '',
|
|
938
|
-
destPort: record.incoming.localPort || 0
|
|
943
|
+
destPort: record.incoming.localPort || 0,
|
|
939
944
|
};
|
|
940
|
-
|
|
945
|
+
|
|
941
946
|
// Check for session tickets if allowSessionTicket is disabled
|
|
942
947
|
if (this.settings.allowSessionTicket === false) {
|
|
943
948
|
// Analyze for session resumption attempt (session ticket or PSK)
|
|
944
|
-
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
945
|
-
|
|
949
|
+
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
950
|
+
renegChunk,
|
|
951
|
+
this.settings.enableTlsDebugLogging
|
|
952
|
+
);
|
|
953
|
+
|
|
946
954
|
if (resumptionInfo.isResumption) {
|
|
947
955
|
// Always log resumption attempt for easier debugging
|
|
948
956
|
// Try to extract SNI for logging
|
|
949
|
-
const extractedSNI = SniHandler.extractSNI(
|
|
957
|
+
const extractedSNI = SniHandler.extractSNI(
|
|
958
|
+
renegChunk,
|
|
959
|
+
this.settings.enableTlsDebugLogging
|
|
960
|
+
);
|
|
950
961
|
console.log(
|
|
951
962
|
`[${connectionId}] Session resumption detected in renegotiation. ` +
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
963
|
+
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
|
|
964
|
+
`SNI value: ${extractedSNI || 'None'}, ` +
|
|
965
|
+
`allowSessionTicket: ${this.settings.allowSessionTicket}`
|
|
955
966
|
);
|
|
956
|
-
|
|
967
|
+
|
|
957
968
|
// Block if there's session resumption without SNI
|
|
958
969
|
if (!resumptionInfo.hasSNI) {
|
|
959
970
|
console.log(
|
|
960
971
|
`[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` +
|
|
961
|
-
|
|
972
|
+
`Terminating connection to force new TLS handshake.`
|
|
962
973
|
);
|
|
963
974
|
this.initiateCleanupOnce(record, 'session_ticket_blocked');
|
|
964
975
|
return;
|
|
@@ -966,14 +977,18 @@ export class PortProxy {
|
|
|
966
977
|
if (this.settings.enableDetailedLogging) {
|
|
967
978
|
console.log(
|
|
968
979
|
`[${connectionId}] Session resumption with SNI detected in renegotiation. ` +
|
|
969
|
-
|
|
980
|
+
`Allowing connection since SNI is present.`
|
|
970
981
|
);
|
|
971
982
|
}
|
|
972
983
|
}
|
|
973
984
|
}
|
|
974
985
|
}
|
|
975
|
-
|
|
976
|
-
const newSNI = SniHandler.extractSNIWithResumptionSupport(
|
|
986
|
+
|
|
987
|
+
const newSNI = SniHandler.extractSNIWithResumptionSupport(
|
|
988
|
+
renegChunk,
|
|
989
|
+
connInfo,
|
|
990
|
+
this.settings.enableTlsDebugLogging
|
|
991
|
+
);
|
|
977
992
|
|
|
978
993
|
// Skip if no SNI was found
|
|
979
994
|
if (!newSNI) return;
|
|
@@ -983,7 +998,7 @@ export class PortProxy {
|
|
|
983
998
|
// Log and terminate the connection for any SNI change
|
|
984
999
|
console.log(
|
|
985
1000
|
`[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
|
|
986
|
-
|
|
1001
|
+
`Terminating connection - SNI domain switching is not allowed.`
|
|
987
1002
|
);
|
|
988
1003
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
989
1004
|
} else if (this.settings.enableDetailedLogging) {
|
|
@@ -1005,11 +1020,15 @@ export class PortProxy {
|
|
|
1005
1020
|
// The renegotiation handler is added when piping is established
|
|
1006
1021
|
// Making it part of setupPiping ensures proper sequencing of event handlers
|
|
1007
1022
|
socket.on('data', renegotiationHandler);
|
|
1008
|
-
|
|
1023
|
+
|
|
1009
1024
|
if (this.settings.enableDetailedLogging) {
|
|
1010
|
-
console.log(
|
|
1025
|
+
console.log(
|
|
1026
|
+
`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
|
|
1027
|
+
);
|
|
1011
1028
|
if (this.settings.allowSessionTicket === false) {
|
|
1012
|
-
console.log(
|
|
1029
|
+
console.log(
|
|
1030
|
+
`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
|
|
1031
|
+
);
|
|
1013
1032
|
}
|
|
1014
1033
|
}
|
|
1015
1034
|
}
|
|
@@ -1176,7 +1195,7 @@ export class PortProxy {
|
|
|
1176
1195
|
try {
|
|
1177
1196
|
// Remove our safe data handler
|
|
1178
1197
|
record.incoming.removeAllListeners('data');
|
|
1179
|
-
|
|
1198
|
+
|
|
1180
1199
|
// Reset the handler references
|
|
1181
1200
|
record.renegotiationHandler = undefined;
|
|
1182
1201
|
} catch (err) {
|
|
@@ -1402,7 +1421,11 @@ export class PortProxy {
|
|
|
1402
1421
|
}
|
|
1403
1422
|
|
|
1404
1423
|
// Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
|
|
1405
|
-
if (
|
|
1424
|
+
if (
|
|
1425
|
+
this.settings.useNetworkProxy &&
|
|
1426
|
+
this.settings.useNetworkProxy.length > 0 &&
|
|
1427
|
+
!this.networkProxy
|
|
1428
|
+
) {
|
|
1406
1429
|
await this.initializeNetworkProxy();
|
|
1407
1430
|
}
|
|
1408
1431
|
|
|
@@ -1410,12 +1433,16 @@ export class PortProxy {
|
|
|
1410
1433
|
if (this.networkProxy) {
|
|
1411
1434
|
await this.networkProxy.start();
|
|
1412
1435
|
console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
|
|
1413
|
-
|
|
1436
|
+
|
|
1414
1437
|
// Log ACME status
|
|
1415
1438
|
if (this.settings.acme?.enabled) {
|
|
1416
|
-
console.log(
|
|
1439
|
+
console.log(
|
|
1440
|
+
`ACME certificate management is enabled (${
|
|
1441
|
+
this.settings.acme.useProduction ? 'Production' : 'Staging'
|
|
1442
|
+
} mode)`
|
|
1443
|
+
);
|
|
1417
1444
|
console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
|
|
1418
|
-
|
|
1445
|
+
|
|
1419
1446
|
// Register domains for ACME certificates if enabled
|
|
1420
1447
|
if (this.networkProxy.options.acme?.enabled) {
|
|
1421
1448
|
console.log('Registering domains with ACME certificate manager...');
|
|
@@ -1489,7 +1516,7 @@ export class PortProxy {
|
|
|
1489
1516
|
|
|
1490
1517
|
// Initialize browser connection tracking
|
|
1491
1518
|
isBrowserConnection: false,
|
|
1492
|
-
domainSwitches: 0,
|
|
1519
|
+
domainSwitches: 0,
|
|
1493
1520
|
};
|
|
1494
1521
|
|
|
1495
1522
|
// Apply keep-alive settings if enabled
|
|
@@ -1536,9 +1563,9 @@ export class PortProxy {
|
|
|
1536
1563
|
|
|
1537
1564
|
// Check if this connection should be forwarded directly to NetworkProxy
|
|
1538
1565
|
// First check port-based forwarding settings
|
|
1539
|
-
let shouldUseNetworkProxy =
|
|
1540
|
-
|
|
1541
|
-
|
|
1566
|
+
let shouldUseNetworkProxy =
|
|
1567
|
+
this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(localPort);
|
|
1568
|
+
|
|
1542
1569
|
// We'll look for domain-specific settings after SNI extraction
|
|
1543
1570
|
|
|
1544
1571
|
if (shouldUseNetworkProxy) {
|
|
@@ -1577,13 +1604,13 @@ export class PortProxy {
|
|
|
1577
1604
|
|
|
1578
1605
|
initialDataReceived = true;
|
|
1579
1606
|
connectionRecord.hasReceivedInitialData = true;
|
|
1580
|
-
|
|
1607
|
+
|
|
1581
1608
|
// Block non-TLS connections on port 443
|
|
1582
1609
|
// Always enforce TLS on standard HTTPS port
|
|
1583
1610
|
if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
|
|
1584
1611
|
console.log(
|
|
1585
1612
|
`[${connectionId}] Non-TLS connection detected on port 443. ` +
|
|
1586
|
-
|
|
1613
|
+
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
1587
1614
|
);
|
|
1588
1615
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
1589
1616
|
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
|
|
@@ -1597,29 +1624,35 @@ export class PortProxy {
|
|
|
1597
1624
|
// Check if this looks like a TLS handshake
|
|
1598
1625
|
if (SniHandler.isTlsHandshake(chunk)) {
|
|
1599
1626
|
connectionRecord.isTLS = true;
|
|
1600
|
-
|
|
1627
|
+
|
|
1601
1628
|
// Check for TLS ClientHello with either no SNI or session tickets
|
|
1602
1629
|
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
|
|
1603
1630
|
// Extract SNI first
|
|
1604
|
-
const extractedSNI = SniHandler.extractSNI(
|
|
1631
|
+
const extractedSNI = SniHandler.extractSNI(
|
|
1632
|
+
chunk,
|
|
1633
|
+
this.settings.enableTlsDebugLogging
|
|
1634
|
+
);
|
|
1605
1635
|
const hasSNI = !!extractedSNI;
|
|
1606
|
-
|
|
1636
|
+
|
|
1607
1637
|
// Analyze for session resumption attempt
|
|
1608
|
-
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
1609
|
-
|
|
1638
|
+
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
1639
|
+
chunk,
|
|
1640
|
+
this.settings.enableTlsDebugLogging
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1610
1643
|
// Always log for debugging purposes
|
|
1611
1644
|
console.log(
|
|
1612
1645
|
`[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` +
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1646
|
+
`Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` +
|
|
1647
|
+
`SNI value: ${extractedSNI || 'None'}, ` +
|
|
1648
|
+
`Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}`
|
|
1616
1649
|
);
|
|
1617
|
-
|
|
1650
|
+
|
|
1618
1651
|
// Block if this is a connection with session resumption but no SNI
|
|
1619
1652
|
if (resumptionInfo.isResumption && !hasSNI) {
|
|
1620
1653
|
console.log(
|
|
1621
1654
|
`[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
|
|
1622
|
-
|
|
1655
|
+
`Terminating connection to force new TLS handshake.`
|
|
1623
1656
|
);
|
|
1624
1657
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
1625
1658
|
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
|
|
@@ -1629,13 +1662,13 @@ export class PortProxy {
|
|
|
1629
1662
|
this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
|
|
1630
1663
|
return;
|
|
1631
1664
|
}
|
|
1632
|
-
|
|
1665
|
+
|
|
1633
1666
|
// Also block if this is a TLS connection without SNI when allowSessionTicket is false
|
|
1634
1667
|
// This forces clients to send SNI which helps with routing
|
|
1635
1668
|
if (!hasSNI && localPort === 443) {
|
|
1636
1669
|
console.log(
|
|
1637
1670
|
`[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
|
|
1638
|
-
|
|
1671
|
+
`Terminating connection to force proper SNI in handshake.`
|
|
1639
1672
|
);
|
|
1640
1673
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
1641
1674
|
connectionRecord.incomingTerminationReason = 'no_sni_blocked';
|
|
@@ -1646,57 +1679,70 @@ export class PortProxy {
|
|
|
1646
1679
|
return;
|
|
1647
1680
|
}
|
|
1648
1681
|
}
|
|
1649
|
-
|
|
1682
|
+
|
|
1650
1683
|
// Try to extract SNI for domain-specific NetworkProxy handling
|
|
1651
1684
|
const connInfo = {
|
|
1652
1685
|
sourceIp: remoteIP,
|
|
1653
1686
|
sourcePort: socket.remotePort || 0,
|
|
1654
1687
|
destIp: socket.localAddress || '',
|
|
1655
|
-
destPort: socket.localPort || 0
|
|
1688
|
+
destPort: socket.localPort || 0,
|
|
1656
1689
|
};
|
|
1657
|
-
|
|
1690
|
+
|
|
1658
1691
|
// Extract SNI to check for domain-specific NetworkProxy settings
|
|
1659
1692
|
const serverName = SniHandler.processTlsPacket(
|
|
1660
|
-
chunk,
|
|
1693
|
+
chunk,
|
|
1661
1694
|
connInfo,
|
|
1662
1695
|
this.settings.enableTlsDebugLogging
|
|
1663
1696
|
);
|
|
1664
|
-
|
|
1697
|
+
|
|
1665
1698
|
if (serverName) {
|
|
1666
1699
|
// If we got an SNI, check for domain-specific NetworkProxy settings
|
|
1667
1700
|
const domainConfig = this.settings.domainConfigs.find((config) =>
|
|
1668
1701
|
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
1669
1702
|
);
|
|
1670
|
-
|
|
1703
|
+
|
|
1671
1704
|
// Save domain config and SNI in connection record
|
|
1672
1705
|
connectionRecord.domainConfig = domainConfig;
|
|
1673
1706
|
connectionRecord.lockedDomain = serverName;
|
|
1674
|
-
|
|
1707
|
+
|
|
1675
1708
|
// Use domain-specific NetworkProxy port if configured
|
|
1676
1709
|
if (domainConfig?.useNetworkProxy) {
|
|
1677
|
-
const networkProxyPort =
|
|
1678
|
-
|
|
1710
|
+
const networkProxyPort =
|
|
1711
|
+
domainConfig.networkProxyPort || this.settings.networkProxyPort;
|
|
1712
|
+
|
|
1679
1713
|
if (this.settings.enableDetailedLogging) {
|
|
1680
1714
|
console.log(
|
|
1681
1715
|
`[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
|
|
1682
1716
|
);
|
|
1683
1717
|
}
|
|
1684
|
-
|
|
1718
|
+
|
|
1685
1719
|
// Forward to NetworkProxy with domain-specific port
|
|
1686
|
-
this.forwardToNetworkProxy(
|
|
1720
|
+
this.forwardToNetworkProxy(
|
|
1721
|
+
connectionId,
|
|
1722
|
+
socket,
|
|
1723
|
+
connectionRecord,
|
|
1724
|
+
chunk,
|
|
1725
|
+
networkProxyPort
|
|
1726
|
+
);
|
|
1687
1727
|
return;
|
|
1688
1728
|
}
|
|
1689
1729
|
}
|
|
1690
|
-
|
|
1730
|
+
|
|
1691
1731
|
// Forward directly to NetworkProxy without domain-specific settings
|
|
1692
1732
|
this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
|
|
1693
1733
|
} else {
|
|
1694
1734
|
// If not TLS, use normal direct connection
|
|
1695
1735
|
console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
|
|
1696
|
-
this.setupDirectConnection(
|
|
1736
|
+
this.setupDirectConnection(
|
|
1737
|
+
connectionId,
|
|
1738
|
+
socket,
|
|
1739
|
+
connectionRecord,
|
|
1740
|
+
undefined,
|
|
1741
|
+
undefined,
|
|
1742
|
+
chunk
|
|
1743
|
+
);
|
|
1697
1744
|
}
|
|
1698
1745
|
});
|
|
1699
|
-
|
|
1700
1746
|
} else {
|
|
1701
1747
|
// For non-NetworkProxy ports, proceed with normal processing
|
|
1702
1748
|
|
|
@@ -1760,9 +1806,9 @@ export class PortProxy {
|
|
|
1760
1806
|
sourceIp: remoteIP,
|
|
1761
1807
|
sourcePort: socket.remotePort || 0,
|
|
1762
1808
|
destIp: socket.localAddress || '',
|
|
1763
|
-
destPort: socket.localPort || 0
|
|
1809
|
+
destPort: socket.localPort || 0,
|
|
1764
1810
|
};
|
|
1765
|
-
|
|
1811
|
+
|
|
1766
1812
|
SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true);
|
|
1767
1813
|
}
|
|
1768
1814
|
}
|
|
@@ -1814,7 +1860,7 @@ export class PortProxy {
|
|
|
1814
1860
|
|
|
1815
1861
|
// Save domain config in connection record
|
|
1816
1862
|
connectionRecord.domainConfig = domainConfig;
|
|
1817
|
-
|
|
1863
|
+
|
|
1818
1864
|
// Check if this domain should use NetworkProxy (domain-specific setting)
|
|
1819
1865
|
if (domainConfig?.useNetworkProxy && this.networkProxy) {
|
|
1820
1866
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -1822,15 +1868,16 @@ export class PortProxy {
|
|
|
1822
1868
|
`[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
|
|
1823
1869
|
);
|
|
1824
1870
|
}
|
|
1825
|
-
|
|
1826
|
-
const networkProxyPort =
|
|
1827
|
-
|
|
1871
|
+
|
|
1872
|
+
const networkProxyPort =
|
|
1873
|
+
domainConfig.networkProxyPort || this.settings.networkProxyPort;
|
|
1874
|
+
|
|
1828
1875
|
if (initialChunk && connectionRecord.isTLS) {
|
|
1829
1876
|
// For TLS connections with initial chunk, forward to NetworkProxy
|
|
1830
1877
|
this.forwardToNetworkProxy(
|
|
1831
|
-
connectionId,
|
|
1832
|
-
socket,
|
|
1833
|
-
connectionRecord,
|
|
1878
|
+
connectionId,
|
|
1879
|
+
socket,
|
|
1880
|
+
connectionRecord,
|
|
1834
1881
|
initialChunk,
|
|
1835
1882
|
networkProxyPort // Pass the domain-specific NetworkProxy port if configured
|
|
1836
1883
|
);
|
|
@@ -1861,7 +1908,10 @@ export class PortProxy {
|
|
|
1861
1908
|
)}`
|
|
1862
1909
|
);
|
|
1863
1910
|
}
|
|
1864
|
-
} else if (
|
|
1911
|
+
} else if (
|
|
1912
|
+
this.settings.defaultAllowedIPs &&
|
|
1913
|
+
this.settings.defaultAllowedIPs.length > 0
|
|
1914
|
+
) {
|
|
1865
1915
|
if (
|
|
1866
1916
|
!isGlobIPAllowed(
|
|
1867
1917
|
remoteIP,
|
|
@@ -1980,13 +2030,32 @@ export class PortProxy {
|
|
|
1980
2030
|
}
|
|
1981
2031
|
|
|
1982
2032
|
initialDataReceived = true;
|
|
1983
|
-
|
|
2033
|
+
|
|
2034
|
+
// ADD THE DEBUGGING CODE RIGHT HERE, BEFORE ANY OTHER PROCESSING
|
|
2035
|
+
if (SniHandler.isClientHello(chunk)) {
|
|
2036
|
+
// Log more details to understand session resumption
|
|
2037
|
+
const resumptionInfo = SniHandler.hasSessionResumption(chunk, true);
|
|
2038
|
+
console.log(
|
|
2039
|
+
`[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}`
|
|
2040
|
+
);
|
|
2041
|
+
|
|
2042
|
+
// Try both extraction methods
|
|
2043
|
+
const standardSNI = SniHandler.extractSNI(chunk, true);
|
|
2044
|
+
const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true);
|
|
2045
|
+
|
|
2046
|
+
console.log(
|
|
2047
|
+
`[${connectionId}] SNI extraction results: standardSNI=${
|
|
2048
|
+
standardSNI || 'none'
|
|
2049
|
+
}, pskSNI=${pskSNI || 'none'}`
|
|
2050
|
+
);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1984
2053
|
// Block non-TLS connections on port 443
|
|
1985
2054
|
// Always enforce TLS on standard HTTPS port
|
|
1986
2055
|
if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
|
|
1987
2056
|
console.log(
|
|
1988
2057
|
`[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
|
|
1989
|
-
|
|
2058
|
+
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
|
1990
2059
|
);
|
|
1991
2060
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
1992
2061
|
connectionRecord.incomingTerminationReason = 'non_tls_blocked';
|
|
@@ -2008,28 +2077,34 @@ export class PortProxy {
|
|
|
2008
2077
|
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
|
2009
2078
|
);
|
|
2010
2079
|
}
|
|
2011
|
-
|
|
2080
|
+
|
|
2012
2081
|
// Check for session tickets if allowSessionTicket is disabled
|
|
2013
2082
|
if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
|
|
2014
2083
|
// Analyze for session resumption attempt
|
|
2015
|
-
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
2016
|
-
|
|
2084
|
+
const resumptionInfo = SniHandler.hasSessionResumption(
|
|
2085
|
+
chunk,
|
|
2086
|
+
this.settings.enableTlsDebugLogging
|
|
2087
|
+
);
|
|
2088
|
+
|
|
2017
2089
|
if (resumptionInfo.isResumption) {
|
|
2018
2090
|
// Always log resumption attempt for easier debugging
|
|
2019
2091
|
// Try to extract SNI for logging
|
|
2020
|
-
const extractedSNI = SniHandler.extractSNI(
|
|
2092
|
+
const extractedSNI = SniHandler.extractSNI(
|
|
2093
|
+
chunk,
|
|
2094
|
+
this.settings.enableTlsDebugLogging
|
|
2095
|
+
);
|
|
2021
2096
|
console.log(
|
|
2022
2097
|
`[${connectionId}] Session resumption detected in SNI handler. ` +
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2098
|
+
`Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
|
|
2099
|
+
`SNI value: ${extractedSNI || 'None'}, ` +
|
|
2100
|
+
`allowSessionTicket: ${this.settings.allowSessionTicket}`
|
|
2026
2101
|
);
|
|
2027
|
-
|
|
2102
|
+
|
|
2028
2103
|
// Block if there's session resumption without SNI
|
|
2029
2104
|
if (!resumptionInfo.hasSNI) {
|
|
2030
2105
|
console.log(
|
|
2031
2106
|
`[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` +
|
|
2032
|
-
|
|
2107
|
+
`Terminating connection to force new TLS handshake.`
|
|
2033
2108
|
);
|
|
2034
2109
|
if (connectionRecord.incomingTerminationReason === null) {
|
|
2035
2110
|
connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
|
|
@@ -2042,7 +2117,7 @@ export class PortProxy {
|
|
|
2042
2117
|
if (this.settings.enableDetailedLogging) {
|
|
2043
2118
|
console.log(
|
|
2044
2119
|
`[${connectionId}] Session resumption with SNI detected in SNI handler. ` +
|
|
2045
|
-
|
|
2120
|
+
`Allowing connection since SNI is present.`
|
|
2046
2121
|
);
|
|
2047
2122
|
}
|
|
2048
2123
|
}
|
|
@@ -2054,16 +2129,17 @@ export class PortProxy {
|
|
|
2054
2129
|
sourceIp: remoteIP,
|
|
2055
2130
|
sourcePort: socket.remotePort || 0,
|
|
2056
2131
|
destIp: socket.localAddress || '',
|
|
2057
|
-
destPort: socket.localPort || 0
|
|
2132
|
+
destPort: socket.localPort || 0,
|
|
2058
2133
|
};
|
|
2059
|
-
|
|
2134
|
+
|
|
2060
2135
|
// Use the new processTlsPacket method for comprehensive handling
|
|
2061
|
-
serverName =
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2136
|
+
serverName =
|
|
2137
|
+
SniHandler.processTlsPacket(
|
|
2138
|
+
chunk,
|
|
2139
|
+
connInfo,
|
|
2140
|
+
this.settings.enableTlsDebugLogging,
|
|
2141
|
+
connectionRecord.lockedDomain // Pass any previously negotiated domain as a hint
|
|
2142
|
+
) || '';
|
|
2067
2143
|
}
|
|
2068
2144
|
|
|
2069
2145
|
// Lock the connection to the negotiated SNI.
|
|
@@ -2392,7 +2468,7 @@ export class PortProxy {
|
|
|
2392
2468
|
console.log('Stopping NetworkProxy...');
|
|
2393
2469
|
await this.networkProxy.stop();
|
|
2394
2470
|
console.log('NetworkProxy stopped successfully');
|
|
2395
|
-
|
|
2471
|
+
|
|
2396
2472
|
// Log ACME shutdown if it was enabled
|
|
2397
2473
|
if (this.settings.acme?.enabled) {
|
|
2398
2474
|
console.log('ACME certificate manager stopped');
|
|
@@ -2417,4 +2493,4 @@ export class PortProxy {
|
|
|
2417
2493
|
|
|
2418
2494
|
console.log('PortProxy shutdown complete.');
|
|
2419
2495
|
}
|
|
2420
|
-
}
|
|
2496
|
+
}
|