@fanboynz/network-scanner 2.0.64 → 2.0.65
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/lib/cloudflare.js +225 -102
- package/lib/nettools.js +85 -57
- package/lib/redirect.js +38 -8
- package/nwss.js +225 -104
- package/package.json +1 -1
- package/scanner-script-org.js +0 -588
package/nwss.js
CHANGED
|
@@ -360,6 +360,12 @@ let adblockEnabled = false;
|
|
|
360
360
|
let adblockMatcher = null;
|
|
361
361
|
let adblockStats = { blocked: 0, allowed: 0 };
|
|
362
362
|
|
|
363
|
+
// Cloudflare scan-wide stats. errorPages counts URLs where the returned page
|
|
364
|
+
// was a Cloudflare-served 5xx origin error (522/523/etc.) — no bypass
|
|
365
|
+
// possible, useful signal for diagnosing dead-origin scans. Named distinct
|
|
366
|
+
// from the local cloudflareStats = getCacheStats() in the debug stats block.
|
|
367
|
+
let cloudflareScanStats = { errorPages: 0 };
|
|
368
|
+
|
|
363
369
|
// Validate --adblock-rules usage - ignore if used incorrectly instead of erroring
|
|
364
370
|
if (adblockRulesMode) {
|
|
365
371
|
if (!outputFile) {
|
|
@@ -478,78 +484,10 @@ if (testValidation) {
|
|
|
478
484
|
}
|
|
479
485
|
}
|
|
480
486
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// Validate referrer_headers format
|
|
487
|
-
for (const site of sites) {
|
|
488
|
-
if (site.referrer_headers && typeof site.referrer_headers === 'object' && !Array.isArray(site.referrer_headers)) {
|
|
489
|
-
const validation = validateReferrerConfig(site.referrer_headers);
|
|
490
|
-
if (!validation.isValid) {
|
|
491
|
-
console.warn(`⚠ Invalid referrer_headers configuration: ${validation.errors.join(', ')}`);
|
|
492
|
-
}
|
|
493
|
-
if (validation.warnings.length > 0) {
|
|
494
|
-
console.warn(`⚠ Referrer warnings: ${validation.warnings.join(', ')}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
// Validate referrer_disable format
|
|
498
|
-
if (site.referrer_disable) {
|
|
499
|
-
const disableValidation = validateReferrerDisable(site.referrer_disable);
|
|
500
|
-
if (!disableValidation.isValid) {
|
|
501
|
-
console.warn(`⚠ Invalid referrer_disable configuration: ${disableValidation.errors.join(', ')}`);
|
|
502
|
-
}
|
|
503
|
-
if (disableValidation.warnings.length > 0) {
|
|
504
|
-
console.warn(`⚠ Referrer disable warnings: ${disableValidation.warnings.join(', ')}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Validate VPN configurations
|
|
510
|
-
for (const site of sites) {
|
|
511
|
-
if (site.vpn) {
|
|
512
|
-
const vpnNorm = normalizeVpnConfig(site.vpn);
|
|
513
|
-
const vpnValidation = validateVpnConfig(vpnNorm);
|
|
514
|
-
if (!vpnValidation.isValid) {
|
|
515
|
-
console.warn(`⚠ Invalid vpn configuration for ${site.url}: ${vpnValidation.errors.join(', ')}`);
|
|
516
|
-
}
|
|
517
|
-
if (vpnValidation.warnings.length > 0) {
|
|
518
|
-
vpnValidation.warnings.forEach(w => console.warn(`⚠ VPN warning for ${site.url}: ${w}`));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
if (site.openvpn) {
|
|
522
|
-
const ovpnNorm = normalizeOvpnConfig(site.openvpn);
|
|
523
|
-
const ovpnValidation = validateOvpnConfig(ovpnNorm);
|
|
524
|
-
if (!ovpnValidation.isValid) {
|
|
525
|
-
console.warn(`⚠ Invalid openvpn configuration for ${site.url}: ${ovpnValidation.errors.join(', ')}`);
|
|
526
|
-
}
|
|
527
|
-
if (ovpnValidation.warnings.length > 0) {
|
|
528
|
-
ovpnValidation.warnings.forEach(w => console.warn(`⚠ OpenVPN warning for ${site.url}: ${w}`));
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
if (site.vpn && site.openvpn) {
|
|
532
|
-
console.warn(`⚠ ${site.url} has both vpn and openvpn configured — only one will be used (vpn takes precedence)`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (validation.isValid) {
|
|
537
|
-
console.log(`${messageColors.success('✅ Configuration is valid!')}`);
|
|
538
|
-
console.log(`${messageColors.info('Summary:')} ${validation.summary.validSites}/${validation.summary.totalSites} sites valid`);
|
|
539
|
-
if (validation.summary.sitesWithWarnings > 0) {
|
|
540
|
-
console.log(`${messageColors.warn('⚠ Warnings:')} ${validation.summary.sitesWithWarnings} sites have warnings`);
|
|
541
|
-
}
|
|
542
|
-
process.exit(0);
|
|
543
|
-
} else {
|
|
544
|
-
console.log(`${messageColors.error('❌ Configuration validation failed!')}`);
|
|
545
|
-
console.log(`${messageColors.error('Errors:')} ${validation.globalErrors.length} global, ${validation.summary.sitesWithErrors} site-specific`);
|
|
546
|
-
process.exit(1);
|
|
547
|
-
}
|
|
548
|
-
} catch (validationErr) {
|
|
549
|
-
console.error(`❌ Validation failed: ${validationErr.message}`);
|
|
550
|
-
process.exit(1);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
487
|
+
// Note: --validate-config is handled further down, AFTER the config file is
|
|
488
|
+
// loaded and `config`/`sites` are populated. Running it here would fail with
|
|
489
|
+
// "Cannot access 'config' before initialization" since those are declared
|
|
490
|
+
// later in the module.
|
|
553
491
|
|
|
554
492
|
if (validateRules || validateRulesFile) {
|
|
555
493
|
const filesToValidate = validateRulesFile ? [validateRulesFile] : [outputFile, compareFile].filter(Boolean);
|
|
@@ -909,10 +847,86 @@ const {
|
|
|
909
847
|
disable_ad_tagging = true,
|
|
910
848
|
max_concurrent_sites = 6,
|
|
911
849
|
resource_cleanup_interval = 80,
|
|
912
|
-
comments: globalComments,
|
|
913
|
-
...otherGlobalConfig
|
|
850
|
+
comments: globalComments,
|
|
851
|
+
...otherGlobalConfig
|
|
914
852
|
} = config;
|
|
915
853
|
|
|
854
|
+
// --validate-config runs here, after `config` and `sites` are populated.
|
|
855
|
+
// Previously this block lived above the config load and triggered a TDZ
|
|
856
|
+
// "Cannot access 'config' before initialization" error.
|
|
857
|
+
if (validateConfig) {
|
|
858
|
+
console.log(`\n${messageColors.processing('Validating configuration file...')}`);
|
|
859
|
+
try {
|
|
860
|
+
const validation = validateFullConfig(config, { forceDebug, silentMode });
|
|
861
|
+
|
|
862
|
+
// Validate referrer_headers format
|
|
863
|
+
for (const site of sites) {
|
|
864
|
+
if (site.referrer_headers && typeof site.referrer_headers === 'object' && !Array.isArray(site.referrer_headers)) {
|
|
865
|
+
const refValidation = validateReferrerConfig(site.referrer_headers);
|
|
866
|
+
if (!refValidation.isValid) {
|
|
867
|
+
console.warn(`⚠ Invalid referrer_headers configuration: ${refValidation.errors.join(', ')}`);
|
|
868
|
+
}
|
|
869
|
+
if (refValidation.warnings.length > 0) {
|
|
870
|
+
console.warn(`⚠ Referrer warnings: ${refValidation.warnings.join(', ')}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// Validate referrer_disable format
|
|
874
|
+
if (site.referrer_disable) {
|
|
875
|
+
const disableValidation = validateReferrerDisable(site.referrer_disable);
|
|
876
|
+
if (!disableValidation.isValid) {
|
|
877
|
+
console.warn(`⚠ Invalid referrer_disable configuration: ${disableValidation.errors.join(', ')}`);
|
|
878
|
+
}
|
|
879
|
+
if (disableValidation.warnings.length > 0) {
|
|
880
|
+
console.warn(`⚠ Referrer disable warnings: ${disableValidation.warnings.join(', ')}`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Validate VPN configurations
|
|
886
|
+
for (const site of sites) {
|
|
887
|
+
if (site.vpn) {
|
|
888
|
+
const vpnNorm = normalizeVpnConfig(site.vpn);
|
|
889
|
+
const vpnValidation = validateVpnConfig(vpnNorm);
|
|
890
|
+
if (!vpnValidation.isValid) {
|
|
891
|
+
console.warn(`⚠ Invalid vpn configuration for ${site.url}: ${vpnValidation.errors.join(', ')}`);
|
|
892
|
+
}
|
|
893
|
+
if (vpnValidation.warnings.length > 0) {
|
|
894
|
+
vpnValidation.warnings.forEach(w => console.warn(`⚠ VPN warning for ${site.url}: ${w}`));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (site.openvpn) {
|
|
898
|
+
const ovpnNorm = normalizeOvpnConfig(site.openvpn);
|
|
899
|
+
const ovpnValidation = validateOvpnConfig(ovpnNorm);
|
|
900
|
+
if (!ovpnValidation.isValid) {
|
|
901
|
+
console.warn(`⚠ Invalid openvpn configuration for ${site.url}: ${ovpnValidation.errors.join(', ')}`);
|
|
902
|
+
}
|
|
903
|
+
if (ovpnValidation.warnings.length > 0) {
|
|
904
|
+
ovpnValidation.warnings.forEach(w => console.warn(`⚠ OpenVPN warning for ${site.url}: ${w}`));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (site.vpn && site.openvpn) {
|
|
908
|
+
console.warn(`⚠ ${site.url} has both vpn and openvpn configured — only one will be used (vpn takes precedence)`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (validation.isValid) {
|
|
913
|
+
console.log(`${messageColors.success('✅ Configuration is valid!')}`);
|
|
914
|
+
console.log(`${messageColors.info('Summary:')} ${validation.summary.validSites}/${validation.summary.totalSites} sites valid`);
|
|
915
|
+
if (validation.summary.sitesWithWarnings > 0) {
|
|
916
|
+
console.log(`${messageColors.warn('⚠ Warnings:')} ${validation.summary.sitesWithWarnings} sites have warnings`);
|
|
917
|
+
}
|
|
918
|
+
process.exit(0);
|
|
919
|
+
} else {
|
|
920
|
+
console.log(`${messageColors.error('❌ Configuration validation failed!')}`);
|
|
921
|
+
console.log(`${messageColors.error('Errors:')} ${validation.globalErrors.length} global, ${validation.summary.sitesWithErrors} site-specific`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
} catch (validationErr) {
|
|
925
|
+
console.error(`❌ Validation failed: ${validationErr.message}`);
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
916
930
|
// Pre-compile global blocked regexes ONCE (used in every processUrl call)
|
|
917
931
|
const globalBlockedRegexes = Array.isArray(globalBlocked)
|
|
918
932
|
? globalBlocked.map(pattern => new RegExp(pattern))
|
|
@@ -3425,7 +3439,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3425
3439
|
}
|
|
3426
3440
|
}
|
|
3427
3441
|
|
|
3428
|
-
const { finalUrl, redirected, redirectChain, originalUrl, redirectDomains } = navigationResult;
|
|
3442
|
+
const { finalUrl, redirected, redirectChain, originalUrl, redirectDomains, httpStatus, cfRay } = navigationResult;
|
|
3429
3443
|
|
|
3430
3444
|
// Check for same-page reload loops BEFORE redirect processing
|
|
3431
3445
|
const loadCount = pageLoadHistory.get(currentUrl) || 0;
|
|
@@ -3534,8 +3548,14 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3534
3548
|
}
|
|
3535
3549
|
}
|
|
3536
3550
|
|
|
3537
|
-
// Handle all Cloudflare protections using the enhanced module
|
|
3538
|
-
|
|
3551
|
+
// Handle all Cloudflare protections using the enhanced module. Pass
|
|
3552
|
+
// httpStatus and cfRay captured at goto time so the outcome log can
|
|
3553
|
+
// surface them — Puppeteer's response object is only available
|
|
3554
|
+
// immediately after page.goto, so handleCloudflareProtection can't
|
|
3555
|
+
// recover them from `page` alone.
|
|
3556
|
+
const cloudflareResult = await handleCloudflareProtection(page, currentUrl, siteConfig, forceDebug, { httpStatus, cfRay });
|
|
3557
|
+
|
|
3558
|
+
if (cloudflareResult.cloudflareErrorPage) cloudflareScanStats.errorPages++;
|
|
3539
3559
|
|
|
3540
3560
|
// Check if Cloudflare handling exceeded max retries and should terminate processing
|
|
3541
3561
|
if (!cloudflareResult.overallSuccess &&
|
|
@@ -3568,7 +3588,10 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3568
3588
|
console.warn(` - ${error}`);
|
|
3569
3589
|
});
|
|
3570
3590
|
// Continue with scan despite Cloudflare issues
|
|
3571
|
-
} else if (cloudflareResult.verificationChallenge?.success && forceDebug) {
|
|
3591
|
+
} else if (cloudflareResult.verificationChallenge?.attempted && cloudflareResult.verificationChallenge?.success && forceDebug) {
|
|
3592
|
+
// Require attempted === true so we don't log "Challenge solved using:
|
|
3593
|
+
// undefined" for pages that had no challenge to solve (success: true
|
|
3594
|
+
// is the natural state for that case).
|
|
3572
3595
|
console.log(formatLogMessage('debug', `[cloudflare] Challenge solved using: ${cloudflareResult.verificationChallenge.method}`));
|
|
3573
3596
|
}
|
|
3574
3597
|
|
|
@@ -4299,27 +4322,39 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4299
4322
|
let forceRestartFlag = false; // Flag to trigger restart on next iteration
|
|
4300
4323
|
|
|
4301
4324
|
const hangDetectionInterval = setInterval(() => {
|
|
4325
|
+
// Progress check, counter, and forceRestartFlag MUST run regardless of
|
|
4326
|
+
// debug mode — previously the entire body was gated on forceDebug, which
|
|
4327
|
+
// made hang recovery a debug-only feature even though the restart
|
|
4328
|
+
// machinery exists for production scans. Only the verbose diagnostic
|
|
4329
|
+
// logs stay debug-gated; the "no progress" warning and the
|
|
4330
|
+
// "triggering restart" error are user-visible recovery events.
|
|
4331
|
+
if (processedUrlCount === lastProcessedCount) {
|
|
4332
|
+
hangCheckCount++;
|
|
4333
|
+
if (forceDebug) {
|
|
4334
|
+
console.log(formatLogMessage('warn', `[HANG CHECK] No progress for ${hangCheckCount * 30}s`));
|
|
4335
|
+
}
|
|
4336
|
+
if (hangCheckCount >= 5) {
|
|
4337
|
+
console.log(formatLogMessage('error', `[HANG CHECK] Hung for 2.5 minutes. Triggering emergency browser restart.`));
|
|
4338
|
+
forceRestartFlag = true; // Set flag instead of exiting
|
|
4339
|
+
hangCheckCount = 0; // Reset counter for next cycle
|
|
4340
|
+
}
|
|
4341
|
+
} else {
|
|
4342
|
+
hangCheckCount = 0;
|
|
4343
|
+
}
|
|
4344
|
+
lastProcessedCount = processedUrlCount;
|
|
4345
|
+
|
|
4346
|
+
// Debug-only diagnostic snapshot
|
|
4302
4347
|
if (forceDebug) {
|
|
4303
4348
|
const currentBatch = Math.floor(currentBatchInfo.batchStart / RESOURCE_CLEANUP_INTERVAL) + 1;
|
|
4304
4349
|
const totalBatches = Math.ceil(totalUrls / RESOURCE_CLEANUP_INTERVAL);
|
|
4305
4350
|
console.log(formatLogMessage('debug', `[HANG CHECK] Processed: ${processedUrlCount}/${totalUrls} URLs, Batch: ${currentBatch}/${totalBatches}, Current batch size: ${currentBatchInfo.batchSize}`));
|
|
4306
4351
|
console.log(formatLogMessage('debug', `[HANG CHECK] URLs since cleanup: ${urlsSinceLastCleanup}, Recent failures: ${results.slice(-3).filter(r => !r.success).length}/3`));
|
|
4307
|
-
|
|
4308
|
-
// Check progress and trigger browser restart if hung
|
|
4309
|
-
if (processedUrlCount === lastProcessedCount) {
|
|
4310
|
-
hangCheckCount++;
|
|
4311
|
-
console.log(formatLogMessage('warn', `[HANG CHECK] No progress for ${hangCheckCount * 30}s`));
|
|
4312
|
-
if (hangCheckCount >= 5) {
|
|
4313
|
-
console.log(formatLogMessage('error', `[HANG CHECK] Hung for 2.5 minutes. Triggering emergency browser restart.`));
|
|
4314
|
-
forceRestartFlag = true; // Set flag instead of exiting
|
|
4315
|
-
hangCheckCount = 0; // Reset counter for next cycle
|
|
4316
|
-
}
|
|
4317
|
-
} else {
|
|
4318
|
-
hangCheckCount = 0;
|
|
4319
|
-
}
|
|
4320
|
-
lastProcessedCount = processedUrlCount;
|
|
4321
4352
|
}
|
|
4322
4353
|
}, 30000);
|
|
4354
|
+
// Don't keep the event loop alive solely for the hang-check interval — the
|
|
4355
|
+
// clearInterval calls at the normal-exit and error paths already cover the
|
|
4356
|
+
// cleanup, this is belt-and-suspenders in case a future refactor moves them.
|
|
4357
|
+
hangDetectionInterval.unref();
|
|
4323
4358
|
|
|
4324
4359
|
// Process URLs in batches with exception handling
|
|
4325
4360
|
let siteGroupIndex = 0;
|
|
@@ -4387,8 +4422,14 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4387
4422
|
!healthCheck.reason?.includes('Scheduled cleanup') &&
|
|
4388
4423
|
(healthCheck.reason?.includes('Critical') || healthCheck.reason?.includes('disconnected'));
|
|
4389
4424
|
|
|
4390
|
-
// Restart
|
|
4391
|
-
|
|
4425
|
+
// Restart conditions split into hang recovery vs proactive triggers.
|
|
4426
|
+
// Hang recovery (forceRestartFlag set by 2.5-min HANG CHECK or a per-URL
|
|
4427
|
+
// timeout) bypasses the urlsSinceLastCleanup > 8 gate — a confirmed hang
|
|
4428
|
+
// needs immediate restart even if we just cleaned up. Proactive triggers
|
|
4429
|
+
// keep the gate to prevent thrashing.
|
|
4430
|
+
const hangRecoveryRestart = forceRestartFlag;
|
|
4431
|
+
const proactiveRestart = (wouldExceedLimit || shouldRestartFromHealth || (hasHighFailureRate && recentResults.length >= 6)) && urlsSinceLastCleanup > 8;
|
|
4432
|
+
if ((hangRecoveryRestart || proactiveRestart) && isNotLastBatch) {
|
|
4392
4433
|
let restartReason = 'Unknown';
|
|
4393
4434
|
if (forceRestartFlag) {
|
|
4394
4435
|
restartReason = 'Emergency restart due to 2.5-minute hang detection';
|
|
@@ -4517,16 +4558,58 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4517
4558
|
console.log(formatLogMessage('debug', `[CONCURRENCY] Starting ${batchSize} concurrent tasks with limit ${MAX_CONCURRENT_SITES}`));
|
|
4518
4559
|
}
|
|
4519
4560
|
|
|
4520
|
-
// Create tasks with timeout protection — skip domains that repeatedly timed out
|
|
4521
|
-
|
|
4561
|
+
// Create tasks with timeout protection — skip domains that repeatedly timed out.
|
|
4562
|
+
// Wrapped in an outer try/finally so processedUrlCount is incremented exactly
|
|
4563
|
+
// once per URL no matter which return/throw path is taken — that turns HANG
|
|
4564
|
+
// CHECK's signal from "did the batch finish?" into "did any URL finish?",
|
|
4565
|
+
// which is what 30-second tick granularity actually needs.
|
|
4566
|
+
const batchTasks = currentBatch.map(task => originalLimit(async () => {
|
|
4522
4567
|
try {
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4568
|
+
// Short-circuit queued URLs once any URL in this batch has triggered a
|
|
4569
|
+
// restart. Without this, the 80-URL batch in the user's hang trace
|
|
4570
|
+
// would have to fail one-by-one at 120s each (~28 min total) before
|
|
4571
|
+
// the boundary restart could fire. Now: first hang fires the flag,
|
|
4572
|
+
// remaining queued URLs return immediately, batch completes, restart.
|
|
4573
|
+
if (forceRestartFlag) {
|
|
4574
|
+
return { url: task.url, rules: [], success: false, error: 'Browser restart pending', skipped: true };
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
try {
|
|
4578
|
+
const taskDomain = new URL(task.url).hostname;
|
|
4579
|
+
if ((domainTimeoutCounts.get(taskDomain) || 0) >= DOMAIN_TIMEOUT_THRESHOLD) {
|
|
4580
|
+
if (!silentMode) console.log(formatLogMessage('info', `Skipping ${task.url} — ${taskDomain} timed out ${DOMAIN_TIMEOUT_THRESHOLD} times`));
|
|
4581
|
+
return { url: task.url, rules: [], success: false, error: 'Domain repeatedly timed out', skipped: true };
|
|
4582
|
+
}
|
|
4583
|
+
} catch {}
|
|
4584
|
+
|
|
4585
|
+
// Per-URL timeout so a single hung processUrl can't block the batch
|
|
4586
|
+
// forever. 120s is well past any legitimate slow page: Cloudflare
|
|
4587
|
+
// adaptive max ~25s, nettools overall ~65s, navigation 15s.
|
|
4588
|
+
const processUrlPromise = processUrl(task.url, task.config, browser);
|
|
4589
|
+
let perUrlTimer;
|
|
4590
|
+
try {
|
|
4591
|
+
return await Promise.race([
|
|
4592
|
+
processUrlPromise,
|
|
4593
|
+
new Promise((_, reject) => {
|
|
4594
|
+
perUrlTimer = setTimeout(() => reject(new Error('Per-URL timeout (120s)')), 120000);
|
|
4595
|
+
})
|
|
4596
|
+
]);
|
|
4597
|
+
} catch (err) {
|
|
4598
|
+
if (err && err.message === 'Per-URL timeout (120s)') {
|
|
4599
|
+
processUrlPromise.catch(() => {});
|
|
4600
|
+
forceRestartFlag = true;
|
|
4601
|
+
return { url: task.url, rules: [], success: false, error: 'Per-URL timeout (120s)', needsImmediateRestart: true };
|
|
4602
|
+
}
|
|
4603
|
+
throw err;
|
|
4604
|
+
} finally {
|
|
4605
|
+
if (perUrlTimer) clearTimeout(perUrlTimer);
|
|
4527
4606
|
}
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4607
|
+
} finally {
|
|
4608
|
+
// Always count completion — even on unexpected throw — so HANG CHECK's
|
|
4609
|
+
// per-tick progress signal stays accurate. Replaces the old
|
|
4610
|
+
// `processedUrlCount += batchSize` that ran after the whole batch.
|
|
4611
|
+
processedUrlCount++;
|
|
4612
|
+
}
|
|
4530
4613
|
}));
|
|
4531
4614
|
|
|
4532
4615
|
let batchResults;
|
|
@@ -4628,7 +4711,8 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4628
4711
|
}
|
|
4629
4712
|
}
|
|
4630
4713
|
|
|
4631
|
-
processedUrlCount
|
|
4714
|
+
// processedUrlCount is now incremented per-URL inside the batchTasks
|
|
4715
|
+
// wrapper above; no batch-level += batchSize here.
|
|
4632
4716
|
urlsSinceLastCleanup += batchSize;
|
|
4633
4717
|
|
|
4634
4718
|
// Force browser restart if any URL had critical errors
|
|
@@ -4809,12 +4893,40 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4809
4893
|
console.log(formatLogMessage('debug', `Cache hit rate: ${cloudflareStats.hitRate}, Total hits: ${cloudflareStats.hits}, Misses: ${cloudflareStats.misses}`));
|
|
4810
4894
|
console.log(formatLogMessage('debug', `Cached detections: ${cloudflareStats.size}`));
|
|
4811
4895
|
}
|
|
4896
|
+
if (cloudflareScanStats.errorPages > 0) {
|
|
4897
|
+
console.log(formatLogMessage('debug', `Cloudflare 5xx origin-error pages: ${cloudflareScanStats.errorPages} (no bypass possible — origin unreachable)`));
|
|
4898
|
+
}
|
|
4812
4899
|
// Log smart cache statistics (if cache is enabled)
|
|
4813
4900
|
// Adblock statistics
|
|
4814
4901
|
if (adblockEnabled) {
|
|
4815
4902
|
console.log(formatLogMessage('debug', '=== Adblock Statistics ==='));
|
|
4816
4903
|
const blockRate = ((adblockStats.blocked / (adblockStats.blocked + adblockStats.allowed)) * 100).toFixed(1);
|
|
4817
4904
|
console.log(formatLogMessage('debug', `Blocked: ${adblockStats.blocked} requests (${blockRate}% block rate), Allowed: ${adblockStats.allowed}`));
|
|
4905
|
+
|
|
4906
|
+
// Engine-specific stats from the matcher itself. Both engines expose
|
|
4907
|
+
// getStats() but with slightly different cache shapes — JS engine
|
|
4908
|
+
// tracks urlCacheSize + resultCacheSize separately, rust wrapper
|
|
4909
|
+
// tracks a single size. Handle both.
|
|
4910
|
+
if (adblockMatcher && typeof adblockMatcher.getStats === 'function') {
|
|
4911
|
+
try {
|
|
4912
|
+
const es = adblockMatcher.getStats();
|
|
4913
|
+
const engine = es.engine || 'js';
|
|
4914
|
+
console.log(formatLogMessage('debug', `Engine: ${engine}${es.fromDiskCache ? ' (loaded from disk cache)' : ''}`));
|
|
4915
|
+
if (es.cache && (es.cache.hits != null || es.cache.misses != null)) {
|
|
4916
|
+
// rust wrapper: single `size`; JS engine: split into urlCacheSize + resultCacheSize
|
|
4917
|
+
const sizeDesc = es.cache.size != null
|
|
4918
|
+
? `${es.cache.size}/${es.cache.maxSize}`
|
|
4919
|
+
: `url ${es.cache.urlCacheSize}, result ${es.cache.resultCacheSize}, cap ${es.cache.maxSize}`;
|
|
4920
|
+
console.log(formatLogMessage('debug', `Matcher cache: ${es.cache.hits} hits / ${es.cache.misses} misses (${es.cache.hitRate}), ${sizeDesc}`));
|
|
4921
|
+
}
|
|
4922
|
+
if (es.exceptions != null && es.exceptions > 0) {
|
|
4923
|
+
console.log(formatLogMessage('debug', `Whitelist exceptions: ${es.exceptions}`));
|
|
4924
|
+
}
|
|
4925
|
+
if (es.errors != null && es.errors > 0) {
|
|
4926
|
+
console.log(formatLogMessage('debug', `Engine errors: ${es.errors}`));
|
|
4927
|
+
}
|
|
4928
|
+
} catch (_) { /* getStats shape mismatch — don't crash the exit path */ }
|
|
4929
|
+
}
|
|
4818
4930
|
}
|
|
4819
4931
|
if (smartCache) {
|
|
4820
4932
|
const cacheStats = smartCache.getStats();
|
|
@@ -4993,8 +5105,17 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4993
5105
|
}
|
|
4994
5106
|
}
|
|
4995
5107
|
|
|
5108
|
+
// Run the same cleanup the SIGINT/SIGTERM emergency handler does, so normal
|
|
5109
|
+
// scan completion isn't left depending on process.exit(0) to override
|
|
5110
|
+
// lingering setInterval handles (the cloudflare detection cache schedules
|
|
5111
|
+
// one that's otherwise only stopped on signal-driven shutdown).
|
|
5112
|
+
try { cleanupCloudflareCache(); } catch (_) {}
|
|
5113
|
+
try { wgDisconnectAll(forceDebug); } catch (_) {}
|
|
5114
|
+
try { ovpnDisconnectAll(forceDebug); } catch (_) {}
|
|
5115
|
+
try { purgeStaleTrackers(); } catch (_) {}
|
|
5116
|
+
|
|
4996
5117
|
// Clean process termination
|
|
4997
5118
|
if (forceDebug) console.log(formatLogMessage('debug', `About to exit process...`));
|
|
4998
5119
|
process.exit(0);
|
|
4999
|
-
|
|
5120
|
+
|
|
5000
5121
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.65",
|
|
4
4
|
"description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
|
|
5
5
|
"main": "nwss.js",
|
|
6
6
|
"scripts": {
|