@fanboynz/network-scanner 2.0.59 → 2.0.61
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/CHANGELOG.md +51 -0
- package/README.md +51 -1
- package/lib/adblock.js +215 -179
- package/lib/colorize.js +3 -1
- package/lib/compare.js +19 -32
- package/lib/domain-cache.js +9 -7
- package/lib/fingerprint.js +41 -11
- package/lib/grep.js +9 -13
- package/lib/nettools.js +177 -42
- package/lib/output.js +17 -30
- package/nwss.js +290 -62
- package/package.json +1 -1
package/nwss.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// puppeteer for browser automation, fs for file system operations, psl for domain parsing.
|
|
4
4
|
// const pLimit = require('p-limit'); // Will be dynamically imported
|
|
5
|
-
const
|
|
5
|
+
const useObscura = process.argv.includes('--use-obscura');
|
|
6
|
+
const usePuppeteerCore = process.argv.includes('--use-puppeteer-core') || useObscura;
|
|
6
7
|
const puppeteer = usePuppeteerCore ? require('puppeteer-core') : require('puppeteer');
|
|
7
8
|
const fs = require('fs');
|
|
8
9
|
const os = require('os');
|
|
@@ -32,7 +33,7 @@ const { shouldIgnoreSimilarDomain, calculateSimilarity } = require('./lib/ignore
|
|
|
32
33
|
// Graceful exit
|
|
33
34
|
const { handleBrowserExit, cleanupChromeTempFiles } = require('./lib/browserexit');
|
|
34
35
|
// Whois & Dig
|
|
35
|
-
const { createNetToolsHandler, createEnhancedDryRunCallback, validateWhoisAvailability, validateDigAvailability } = require('./lib/nettools');
|
|
36
|
+
const { createNetToolsHandler, createEnhancedDryRunCallback, validateWhoisAvailability, validateDigAvailability, enableDiskCache, getDnsCacheStats } = require('./lib/nettools');
|
|
36
37
|
// File compare
|
|
37
38
|
const { loadComparisonRules, filterUniqueRules } = require('./lib/compare');
|
|
38
39
|
// CDP functionality
|
|
@@ -104,12 +105,12 @@ const CONCURRENCY_LIMITS = Object.freeze({
|
|
|
104
105
|
|
|
105
106
|
// V8 Optimization: Use Map for user agent lookups instead of object
|
|
106
107
|
const USER_AGENTS = Object.freeze(new Map([
|
|
107
|
-
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
108
|
-
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
109
|
-
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
110
|
-
['firefox', "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:
|
|
111
|
-
['firefox_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:
|
|
112
|
-
['firefox_linux', "Mozilla/5.0 (X11; Linux x86_64; rv:
|
|
108
|
+
['chrome', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"],
|
|
109
|
+
['chrome_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"],
|
|
110
|
+
['chrome_linux', "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"],
|
|
111
|
+
['firefox', "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:148.0) Gecko/20100101 Firefox/148.0"],
|
|
112
|
+
['firefox_mac', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0"],
|
|
113
|
+
['firefox_linux', "Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0"],
|
|
113
114
|
['safari', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15"]
|
|
114
115
|
]));
|
|
115
116
|
|
|
@@ -177,6 +178,82 @@ if (args.length === 0) {
|
|
|
177
178
|
args.push('--help');
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
// --- .nwssconfig support: inject per-config settings into args ---
|
|
182
|
+
const NWSSCONFIG_PATH = path.join(__dirname, '.nwssconfig');
|
|
183
|
+
if (fs.existsSync(NWSSCONFIG_PATH)) {
|
|
184
|
+
try {
|
|
185
|
+
const nwssConfig = JSON.parse(fs.readFileSync(NWSSCONFIG_PATH, 'utf-8'));
|
|
186
|
+
// Find which config file is being used (--custom-json <file> or positional .json arg)
|
|
187
|
+
const customJsonIdx = args.findIndex(arg => arg === '--custom-json');
|
|
188
|
+
const configFilename = (customJsonIdx !== -1 && args[customJsonIdx + 1])
|
|
189
|
+
? args[customJsonIdx + 1]
|
|
190
|
+
: args.find(a => a.endsWith('.json') && !a.startsWith('--'));
|
|
191
|
+
|
|
192
|
+
if (configFilename && nwssConfig.configs && nwssConfig.configs[configFilename]) {
|
|
193
|
+
const settings = nwssConfig.configs[configFilename];
|
|
194
|
+
const originalArgs = args.join(' ');
|
|
195
|
+
|
|
196
|
+
// Map settings keys to CLI flags — only inject if not already in args
|
|
197
|
+
const settingsMap = {
|
|
198
|
+
output: ['-o', '--output'],
|
|
199
|
+
max_concurrent: ['--max-concurrent'],
|
|
200
|
+
dns_cache: ['--dns-cache'],
|
|
201
|
+
cache_requests: ['--cache-requests'],
|
|
202
|
+
dumpurls: ['--dumpurls'],
|
|
203
|
+
remove_tempfiles: ['--remove-tempfiles'],
|
|
204
|
+
color: ['--color'],
|
|
205
|
+
remove_dupes: ['--remove-dupes', '--remove-dubes'],
|
|
206
|
+
'remove-dupes': ['--remove-dupes', '--remove-dubes'],
|
|
207
|
+
'remove-dubes': ['--remove-dupes', '--remove-dubes'],
|
|
208
|
+
compress_logs: ['--compress-logs'],
|
|
209
|
+
debug: ['--debug'],
|
|
210
|
+
silent: ['--silent'],
|
|
211
|
+
verbose: ['--verbose'],
|
|
212
|
+
headful: ['--headful'],
|
|
213
|
+
keep_open: ['--keep-open'],
|
|
214
|
+
dry_run: ['--dry-run'],
|
|
215
|
+
titles: ['--titles'],
|
|
216
|
+
sub_domains: ['--sub-domains'],
|
|
217
|
+
no_interact: ['--no-interact'],
|
|
218
|
+
ghost_cursor: ['--ghost-cursor'],
|
|
219
|
+
plain: ['--plain'],
|
|
220
|
+
cdp: ['--cdp'],
|
|
221
|
+
dnsmasq: ['--dnsmasq'],
|
|
222
|
+
unbound: ['--unbound'],
|
|
223
|
+
privoxy: ['--privoxy'],
|
|
224
|
+
pihole: ['--pihole'],
|
|
225
|
+
eval_on_doc: ['--eval-on-doc'],
|
|
226
|
+
use_puppeteer_core: ['--use-puppeteer-core'],
|
|
227
|
+
ignore_cache: ['--ignore-cache'],
|
|
228
|
+
clear_cache: ['--clear-cache'],
|
|
229
|
+
block_ads: ['--block-ads'],
|
|
230
|
+
compare: ['--compare'],
|
|
231
|
+
localhost: ['--localhost'],
|
|
232
|
+
append: ['--append']
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const [key, flags] of Object.entries(settingsMap)) {
|
|
236
|
+
// Support both underscore and hyphen variants (e.g. dns_cache or dns-cache)
|
|
237
|
+
const value = settings[key] !== undefined ? settings[key]
|
|
238
|
+
: settings[key.replace(/_/g, '-')] !== undefined ? settings[key.replace(/_/g, '-')]
|
|
239
|
+
: settings[key.replace(/-/g, '_')] !== undefined ? settings[key.replace(/-/g, '_')]
|
|
240
|
+
: undefined;
|
|
241
|
+
if (value === undefined) continue;
|
|
242
|
+
// Skip if any variant of the flag is already in CLI args
|
|
243
|
+
if (flags.some(f => originalArgs.includes(f))) continue;
|
|
244
|
+
|
|
245
|
+
if (typeof value === 'boolean') {
|
|
246
|
+
if (value) args.push(flags[flags.length - 1]);
|
|
247
|
+
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
248
|
+
args.push(flags[flags.length - 1], String(value));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.error(`Warning: Failed to parse .nwssconfig: ${e.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
180
257
|
const headfulMode = args.includes('--headful');
|
|
181
258
|
const SOURCES_FOLDER = 'sources';
|
|
182
259
|
|
|
@@ -208,6 +285,12 @@ if (localhostIndex !== -1) {
|
|
|
208
285
|
localhostIP = args[localhostIndex].includes('=') ? args[localhostIndex].split('=')[1] : '127.0.0.1';
|
|
209
286
|
}
|
|
210
287
|
const keepBrowserOpen = args.includes('--keep-open');
|
|
288
|
+
const loadExtensionPaths = [];
|
|
289
|
+
args.forEach((arg, idx) => {
|
|
290
|
+
if (arg === '--load-extension' && args[idx + 1] && !args[idx + 1].startsWith('--')) {
|
|
291
|
+
loadExtensionPaths.push(path.resolve(args[idx + 1]));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
211
294
|
const disableInteract = args.includes('--no-interact');
|
|
212
295
|
const globalGhostCursor = args.includes('--ghost-cursor');
|
|
213
296
|
const plainOutput = args.includes('--plain');
|
|
@@ -229,6 +312,8 @@ let cleanRules = args.includes('--clean-rules');
|
|
|
229
312
|
const clearCache = args.includes('--clear-cache');
|
|
230
313
|
const ignoreCache = args.includes('--ignore-cache');
|
|
231
314
|
const cacheRequests = args.includes('--cache-requests');
|
|
315
|
+
const dnsCacheMode = args.includes('--dns-cache');
|
|
316
|
+
if (dnsCacheMode) enableDiskCache();
|
|
232
317
|
|
|
233
318
|
let validateRulesFile = null;
|
|
234
319
|
const validateRulesIndex = args.findIndex(arg => arg === '--validate-rules');
|
|
@@ -499,22 +584,38 @@ if (validateRules || validateRulesFile) {
|
|
|
499
584
|
}
|
|
500
585
|
}
|
|
501
586
|
|
|
502
|
-
// Parse --block-ads argument for request-level ad blocking
|
|
587
|
+
// Parse --block-ads argument for request-level ad blocking (supports comma-separated lists)
|
|
503
588
|
const blockAdsIndex = args.findIndex(arg => arg.startsWith('--block-ads'));
|
|
504
589
|
if (blockAdsIndex !== -1) {
|
|
505
|
-
const
|
|
506
|
-
? args[blockAdsIndex].split('=')[1]
|
|
590
|
+
const rulesArg = args[blockAdsIndex].includes('=')
|
|
591
|
+
? args[blockAdsIndex].split('=')[1]
|
|
507
592
|
: args[blockAdsIndex + 1];
|
|
508
|
-
|
|
509
|
-
if (!
|
|
510
|
-
console.log(
|
|
593
|
+
|
|
594
|
+
if (!rulesArg) {
|
|
595
|
+
console.log('Error: No adblock rules file specified');
|
|
511
596
|
process.exit(1);
|
|
512
597
|
}
|
|
513
|
-
|
|
598
|
+
|
|
599
|
+
const rulesFiles = rulesArg.split(',').map(f => f.trim()).filter(f => f);
|
|
600
|
+
for (const file of rulesFiles) {
|
|
601
|
+
if (!fs.existsSync(file)) {
|
|
602
|
+
console.log(`Error: Adblock rules file not found: ${file}`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Concatenate multiple lists into a single temp file for the parser
|
|
608
|
+
let rulesFile = rulesFiles[0];
|
|
609
|
+
if (rulesFiles.length > 1) {
|
|
610
|
+
rulesFile = path.join(os.tmpdir(), `nwss-adblock-combined-${Date.now()}.txt`);
|
|
611
|
+
const combined = rulesFiles.map(f => fs.readFileSync(f, 'utf-8')).join('\n');
|
|
612
|
+
fs.writeFileSync(rulesFile, combined);
|
|
613
|
+
}
|
|
614
|
+
|
|
514
615
|
adblockEnabled = true;
|
|
515
616
|
adblockMatcher = parseAdblockRules(rulesFile, { enableLogging: forceDebug });
|
|
516
617
|
const stats = adblockMatcher.getStats();
|
|
517
|
-
if (!silentMode) console.log(messageColors.success(`Adblock enabled: Loaded ${stats.total} blocking rules from ${
|
|
618
|
+
if (!silentMode) console.log(messageColors.success(`Adblock enabled: Loaded ${stats.total} blocking rules from ${rulesFiles.length} list${rulesFiles.length > 1 ? 's' : ''}`));
|
|
518
619
|
}
|
|
519
620
|
|
|
520
621
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -541,6 +642,12 @@ Request Blocking:
|
|
|
541
642
|
--block-ads=<file> Block ads/trackers using EasyList format rules (||domain.com^, /ads/*, etc)
|
|
542
643
|
Works at request-level for maximum performance
|
|
543
644
|
|
|
645
|
+
Per-config settings file (.nwssconfig):
|
|
646
|
+
Place a .nwssconfig file in the project root to define per-config settings.
|
|
647
|
+
When a config filename matches a key in .nwssconfig, those settings are used.
|
|
648
|
+
CLI flags merge with and override .nwssconfig settings.
|
|
649
|
+
See README.md for format details.
|
|
650
|
+
|
|
544
651
|
General Options:
|
|
545
652
|
--verbose Force verbose mode globally
|
|
546
653
|
--debug Force debug mode globally
|
|
@@ -556,6 +663,9 @@ General Options:
|
|
|
556
663
|
--headful Launch browser with GUI (not headless)
|
|
557
664
|
--keep-open Keep browser open after scan completes (use with --headful)
|
|
558
665
|
--use-puppeteer-core Use puppeteer-core with system Chrome instead of bundled Chromium
|
|
666
|
+
--use-obscura Connect to running Obscura CDP server (ws://127.0.0.1:9222 or OBSCURA_WS env)
|
|
667
|
+
Skips fingerprint injection — Obscura provides built-in stealth
|
|
668
|
+
--load-extension <path> Load unpacked Chrome extension from directory
|
|
559
669
|
--cdp Enable Chrome DevTools Protocol logging (now per-page if enabled)
|
|
560
670
|
--remove-dupes Remove duplicate domains from output (only with -o)
|
|
561
671
|
--eval-on-doc Globally enable evaluateOnNewDocument() for Fetch/XHR interception
|
|
@@ -567,6 +677,7 @@ General Options:
|
|
|
567
677
|
|
|
568
678
|
Validation Options:
|
|
569
679
|
--cache-requests Cache HTTP requests to avoid re-requesting same URLs within scan
|
|
680
|
+
--dns-cache Persist dig/whois results to disk between runs (3hr/4hr TTL)
|
|
570
681
|
--validate-config Validate config.json file and exit
|
|
571
682
|
--validate-rules [file] Validate rule file format (uses --output/--compare files if no file specified)
|
|
572
683
|
--clean-rules [file] Clean rule files by removing invalid lines and optionally duplicates (uses --output/--compare files if no file specified)
|
|
@@ -583,6 +694,7 @@ Global config.json options:
|
|
|
583
694
|
ignore_similar_ignored_domains: true/false Ignore domains similar to ignoreDomains list (default: true)
|
|
584
695
|
max_concurrent_sites: 8 Maximum concurrent site processing (1-50, default: 8)
|
|
585
696
|
resource_cleanup_interval: 80 Browser restart interval in URLs processed (1-1000, default: 80)
|
|
697
|
+
disable_ad_tagging: true/false Disable Chrome AdTagging to prevent ad frame throttling (default: true)
|
|
586
698
|
|
|
587
699
|
Per-site config.json options:
|
|
588
700
|
url: "site" or ["site1", "site2"] Single URL or list of URLs
|
|
@@ -748,7 +860,8 @@ const {
|
|
|
748
860
|
whois_server_mode = 'random',
|
|
749
861
|
ignore_similar = true,
|
|
750
862
|
ignore_similar_threshold = 80,
|
|
751
|
-
ignore_similar_ignored_domains = true,
|
|
863
|
+
ignore_similar_ignored_domains = true,
|
|
864
|
+
disable_ad_tagging = true,
|
|
752
865
|
max_concurrent_sites = 6,
|
|
753
866
|
resource_cleanup_interval = 80,
|
|
754
867
|
comments: globalComments,
|
|
@@ -760,6 +873,23 @@ const globalBlockedRegexes = Array.isArray(globalBlocked)
|
|
|
760
873
|
? globalBlocked.map(pattern => new RegExp(pattern))
|
|
761
874
|
: [];
|
|
762
875
|
|
|
876
|
+
// Cache compiled regexes by pattern string — avoids recompiling same patterns across URLs
|
|
877
|
+
const _compiledRegexCache = new Map();
|
|
878
|
+
function getCompiledRegex(pattern) {
|
|
879
|
+
let compiled = _compiledRegexCache.get(pattern);
|
|
880
|
+
if (!compiled) {
|
|
881
|
+
compiled = new RegExp(pattern.replace(/^\/(.*)\/$/, '$1'));
|
|
882
|
+
if (_compiledRegexCache.size > 2000) _compiledRegexCache.clear();
|
|
883
|
+
_compiledRegexCache.set(pattern, compiled);
|
|
884
|
+
}
|
|
885
|
+
return compiled;
|
|
886
|
+
}
|
|
887
|
+
function getCompiledRegexes(patterns) {
|
|
888
|
+
if (!patterns) return [];
|
|
889
|
+
const arr = Array.isArray(patterns) ? patterns : [patterns];
|
|
890
|
+
return arr.map(p => getCompiledRegex(p));
|
|
891
|
+
}
|
|
892
|
+
|
|
763
893
|
// Pre-split ignoreDomains into exact Set (O(1) lookup) and wildcard array
|
|
764
894
|
const _ignoreDomainsExact = new Set();
|
|
765
895
|
const _ignoreDomainsWildcard = [];
|
|
@@ -1088,12 +1218,19 @@ if (forceDebug && globalComments) {
|
|
|
1088
1218
|
* @param {string} url - The URL string to parse.
|
|
1089
1219
|
* @returns {string} The root domain, or the original hostname if parsing fails (e.g., for IP addresses or invalid URLs), or an empty string on error.
|
|
1090
1220
|
*/
|
|
1221
|
+
const _rootDomainCache = new Map();
|
|
1091
1222
|
function getRootDomain(url) {
|
|
1223
|
+
const cached = _rootDomainCache.get(url);
|
|
1224
|
+
if (cached !== undefined) return cached;
|
|
1092
1225
|
try {
|
|
1093
1226
|
const { hostname } = new URL(url);
|
|
1094
1227
|
const parsed = psl.parse(hostname);
|
|
1095
|
-
|
|
1228
|
+
const result = parsed.domain || hostname;
|
|
1229
|
+
if (_rootDomainCache.size > 5000) _rootDomainCache.clear();
|
|
1230
|
+
_rootDomainCache.set(url, result);
|
|
1231
|
+
return result;
|
|
1096
1232
|
} catch {
|
|
1233
|
+
_rootDomainCache.set(url, '');
|
|
1097
1234
|
return '';
|
|
1098
1235
|
}
|
|
1099
1236
|
}
|
|
@@ -1388,6 +1525,23 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1388
1525
|
* @returns {Promise<import('puppeteer').Browser>} Browser instance
|
|
1389
1526
|
*/
|
|
1390
1527
|
async function createBrowser(extraArgs = []) {
|
|
1528
|
+
// Obscura mode: connect to a running Obscura CDP server instead of launching Chrome
|
|
1529
|
+
if (useObscura) {
|
|
1530
|
+
const obscuraEndpoint = process.env.OBSCURA_WS || 'ws://127.0.0.1:9222/devtools/browser';
|
|
1531
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Connecting to Obscura at ${obscuraEndpoint}`));
|
|
1532
|
+
try {
|
|
1533
|
+
const browser = await puppeteer.connect({ browserWSEndpoint: obscuraEndpoint });
|
|
1534
|
+
if (!silentMode) console.log(messageColors.success(`Connected to Obscura CDP at ${obscuraEndpoint}`));
|
|
1535
|
+
browser._nwssUserDataDir = null; // No temp dir to clean
|
|
1536
|
+
browser._nwssIsObscura = true;
|
|
1537
|
+
return browser;
|
|
1538
|
+
} catch (err) {
|
|
1539
|
+
console.error(formatLogMessage('error', `Failed to connect to Obscura: ${err.message}`));
|
|
1540
|
+
console.error(formatLogMessage('error', `Start Obscura first: obscura serve --port 9222 --stealth`));
|
|
1541
|
+
process.exit(1);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1391
1545
|
// Create temporary user data directory that we can fully control and clean up
|
|
1392
1546
|
const tempUserDataDir = path.join(os.tmpdir(), `puppeteer-${Date.now()}-${Math.random().toString(36).substring(7)}`);
|
|
1393
1547
|
userDataDir = tempUserDataDir; // Store for cleanup tracking (use outer scope variable)
|
|
@@ -1460,7 +1614,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1460
1614
|
'--disable-blink-features=AutomationControlled',
|
|
1461
1615
|
'--no-first-run',
|
|
1462
1616
|
'--disable-default-apps',
|
|
1463
|
-
'--disable-component-extensions-with-background-pages',
|
|
1617
|
+
...(keepBrowserOpen ? [] : ['--disable-component-extensions-with-background-pages']),
|
|
1464
1618
|
// HIGH IMPACT: Normal Chrome behavior simulation
|
|
1465
1619
|
'--password-store=basic',
|
|
1466
1620
|
'--use-mock-keychain',
|
|
@@ -1474,30 +1628,29 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1474
1628
|
'--disable-background-downloads',
|
|
1475
1629
|
// DISK I/O REDUCTION: Eliminate unnecessary Chrome disk writes
|
|
1476
1630
|
'--disable-breakpad', // No crash dump files
|
|
1477
|
-
'--disable-component-update', // No component update downloads
|
|
1631
|
+
...(keepBrowserOpen ? [] : ['--disable-component-update']), // No component update downloads
|
|
1478
1632
|
'--disable-logging', // No Chrome internal log files
|
|
1479
1633
|
'--log-level=3', // Fatal errors only (suppresses verbose disk logging)
|
|
1480
1634
|
'--no-service-autorun', // No background service disk activity
|
|
1481
1635
|
'--disable-domain-reliability', // No reliability monitor disk writes
|
|
1482
|
-
// PERFORMANCE:
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
'--disable-features=BackForwardCache,AcceptCHFrame',
|
|
1636
|
+
// PERFORMANCE: Disable non-essential Chrome features in a single flag
|
|
1637
|
+
// IMPORTANT: Chrome only reads the LAST --disable-features flag, so combine all into one
|
|
1638
|
+
`--disable-features=AudioServiceOutOfProcess,VizDisplayCompositor,TranslateUI,BlinkGenPropertyTrees,Translate,BackForwardCache,AcceptCHFrame,SafeBrowsing,HttpsFirstBalancedModeAutoEnable,site-per-process,PaintHolding${disable_ad_tagging ? ',AdTagging' : ''}`,
|
|
1486
1639
|
'--disable-ipc-flooding-protection',
|
|
1487
1640
|
'--aggressive-cache-discard',
|
|
1488
1641
|
'--memory-pressure-off',
|
|
1489
1642
|
'--max_old_space_size=2048', // V8 heap limit
|
|
1490
1643
|
'--disable-prompt-on-repost', // Fixes form popup on page reload
|
|
1491
|
-
'--disable-background-networking',
|
|
1644
|
+
...(keepBrowserOpen ? [] : ['--disable-background-networking']),
|
|
1492
1645
|
'--no-sandbox',
|
|
1493
1646
|
'--disable-setuid-sandbox',
|
|
1494
|
-
'--disable-features=SafeBrowsing',
|
|
1495
1647
|
'--disable-dev-shm-usage',
|
|
1496
|
-
'--disable-sync',
|
|
1648
|
+
...(keepBrowserOpen ? [] : ['--disable-sync']),
|
|
1497
1649
|
'--mute-audio',
|
|
1498
1650
|
'--disable-translate',
|
|
1499
1651
|
'--window-size=1920,1080',
|
|
1500
|
-
'--disable-extensions',
|
|
1652
|
+
...(keepBrowserOpen ? [] : ['--disable-extensions', '--disable-component-update']),
|
|
1653
|
+
...(loadExtensionPaths.length ? [`--load-extension=${loadExtensionPaths.join(',')}`, '--enable-extensions'] : []),
|
|
1501
1654
|
'--no-default-browser-check',
|
|
1502
1655
|
'--safebrowsing-disable-auto-update',
|
|
1503
1656
|
'--ignore-ssl-errors',
|
|
@@ -1506,18 +1659,15 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1506
1659
|
'--ignore-certificate-errors-ca-list',
|
|
1507
1660
|
'--disable-web-security',
|
|
1508
1661
|
'--allow-running-insecure-content',
|
|
1509
|
-
'--disable-features=HttpsFirstBalancedModeAutoEnable',
|
|
1510
1662
|
// Puppeteer 23.x: Enhanced performance and stability args
|
|
1511
1663
|
'--disable-renderer-backgrounding',
|
|
1512
1664
|
'--disable-backgrounding-occluded-windows',
|
|
1513
1665
|
'--disable-background-timer-throttling',
|
|
1514
|
-
'--disable-features=site-per-process', // Better for single-site scanning
|
|
1515
1666
|
'--no-zygote', // Better process isolation
|
|
1516
1667
|
// PERFORMANCE: Process and memory reduction for high concurrency
|
|
1517
1668
|
'--renderer-process-limit=10', // Cap renderer processes (default: unlimited)
|
|
1518
1669
|
'--disable-accelerated-2d-canvas', // Software canvas only (we spoof it anyway)
|
|
1519
1670
|
'--disable-hang-monitor', // Remove per-renderer hang check overhead
|
|
1520
|
-
'--disable-features=PaintHolding', // Don't hold frames in renderer memory
|
|
1521
1671
|
'--js-flags=--max-old-space-size=512', // Cap V8 heap per renderer to 512MB
|
|
1522
1672
|
...extraArgs,
|
|
1523
1673
|
],
|
|
@@ -2207,11 +2357,16 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2207
2357
|
}
|
|
2208
2358
|
|
|
2209
2359
|
// --- Apply all fingerprint spoofing (user agent, Brave, fingerprint protection) ---
|
|
2360
|
+
// Skip when using Obscura — it has built-in stealth that conflicts with our injection
|
|
2210
2361
|
try {
|
|
2211
|
-
|
|
2362
|
+
if (!useObscura) {
|
|
2363
|
+
await applyAllFingerprintSpoofing(page, siteConfig, forceDebug, currentUrl);
|
|
2364
|
+
} else if (forceDebug) {
|
|
2365
|
+
console.log(formatLogMessage('debug', `Skipping fingerprint injection — Obscura provides built-in stealth`));
|
|
2366
|
+
}
|
|
2212
2367
|
|
|
2213
|
-
// Client Hints protection for Chrome user agents
|
|
2214
|
-
if (siteConfig.userAgent && siteConfig.userAgent.toLowerCase().includes('chrome')) {
|
|
2368
|
+
// Client Hints protection for Chrome user agents (skipped under Obscura — it sets its own)
|
|
2369
|
+
if (!useObscura && siteConfig.userAgent && siteConfig.userAgent.toLowerCase().includes('chrome')) {
|
|
2215
2370
|
const userAgentKey = siteConfig.userAgent.toLowerCase();
|
|
2216
2371
|
let platform = 'Windows';
|
|
2217
2372
|
let platformVersion = '15.0.0';
|
|
@@ -2228,14 +2383,14 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2228
2383
|
}
|
|
2229
2384
|
|
|
2230
2385
|
await page.setExtraHTTPHeaders({
|
|
2231
|
-
'Sec-CH-UA': '"Not:A-Brand";v="99", "Google Chrome";v="
|
|
2386
|
+
'Sec-CH-UA': '"Not:A-Brand";v="99", "Google Chrome";v="146", "Chromium";v="146"',
|
|
2232
2387
|
'Sec-CH-UA-Platform': `"${platform}"`,
|
|
2233
2388
|
'Sec-CH-UA-Platform-Version': `"${platformVersion}"`,
|
|
2234
2389
|
'Sec-CH-UA-Mobile': '?0',
|
|
2235
2390
|
'Sec-CH-UA-Arch': `"${arch}"`,
|
|
2236
2391
|
'Sec-CH-UA-Bitness': '"64"',
|
|
2237
|
-
'Sec-CH-UA-Full-Version': '"
|
|
2238
|
-
'Sec-CH-UA-Full-Version-List': '"Not:A-Brand";v="99.0.0.0", "Google Chrome";v="
|
|
2392
|
+
'Sec-CH-UA-Full-Version': '"146.0.0.0"',
|
|
2393
|
+
'Sec-CH-UA-Full-Version-List': '"Not:A-Brand";v="99.0.0.0", "Google Chrome";v="146.0.0.0", "Chromium";v="146.0.0.0"'
|
|
2239
2394
|
});
|
|
2240
2395
|
}
|
|
2241
2396
|
} catch (fingerprintErr) {
|
|
@@ -2248,11 +2403,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2248
2403
|
}
|
|
2249
2404
|
}
|
|
2250
2405
|
|
|
2251
|
-
const regexes =
|
|
2252
|
-
? siteConfig.filterRegex.map(r => new RegExp(r.replace(/^\/(.*)\/$/, '$1')))
|
|
2253
|
-
: siteConfig.filterRegex
|
|
2254
|
-
? [new RegExp(siteConfig.filterRegex.replace(/^\/(.*)\/$/, '$1'))]
|
|
2255
|
-
: [];
|
|
2406
|
+
const regexes = getCompiledRegexes(siteConfig.filterRegex);
|
|
2256
2407
|
|
|
2257
2408
|
// NEW: Get regex_and setting (defaults to false for backward compatibility)
|
|
2258
2409
|
const useRegexAnd = siteConfig.regex_and === true;
|
|
@@ -2400,7 +2551,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2400
2551
|
}
|
|
2401
2552
|
|
|
2402
2553
|
const blockedRegexes = Array.isArray(siteConfig.blocked)
|
|
2403
|
-
? siteConfig.blocked.map(pattern =>
|
|
2554
|
+
? siteConfig.blocked.map(pattern => getCompiledRegex(pattern))
|
|
2404
2555
|
: [];
|
|
2405
2556
|
|
|
2406
2557
|
// Combine site-specific with pre-compiled global blocked patterns
|
|
@@ -3177,7 +3328,23 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3177
3328
|
? { ...defaultGotoOptions, ...siteConfig.goto_options } : defaultGotoOptions;
|
|
3178
3329
|
|
|
3179
3330
|
// Enhanced navigation with redirect handling - passes existing gotoOptions
|
|
3180
|
-
|
|
3331
|
+
let navigationResult;
|
|
3332
|
+
try {
|
|
3333
|
+
navigationResult = await navigateWithRedirectHandling(page, currentUrl, siteConfig, gotoOptions, forceDebug, formatLogMessage);
|
|
3334
|
+
} catch (navErr) {
|
|
3335
|
+
// Only retry on genuine timeouts, not chrome-error:// redirects
|
|
3336
|
+
let pageUrl = '';
|
|
3337
|
+
try { if (!page.isClosed()) pageUrl = page.url(); } catch {}
|
|
3338
|
+
const isPopupFailure = navErr.message.includes('chrome-error://') || navErr.message.includes('invalid URL') ||
|
|
3339
|
+
pageUrl.startsWith('chrome-error://') || pageUrl === 'about:blank';
|
|
3340
|
+
if ((navErr.message.includes('timeout') || navErr.message.includes('Timeout')) && !isPopupFailure) {
|
|
3341
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Navigation timeout, retrying with waitUntil:networkidle2 for ${currentUrl}`));
|
|
3342
|
+
const fallbackOptions = { ...gotoOptions, waitUntil: 'networkidle2', timeout: Math.min(timeout, 10000) };
|
|
3343
|
+
navigationResult = await navigateWithRedirectHandling(page, currentUrl, siteConfig, fallbackOptions, forceDebug, formatLogMessage);
|
|
3344
|
+
} else {
|
|
3345
|
+
throw navErr;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3181
3348
|
|
|
3182
3349
|
const { finalUrl, redirected, redirectChain, originalUrl, redirectDomains } = navigationResult;
|
|
3183
3350
|
|
|
@@ -3233,7 +3400,8 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3233
3400
|
}
|
|
3234
3401
|
|
|
3235
3402
|
if (originalDomain !== finalDomain) {
|
|
3236
|
-
|
|
3403
|
+
const isPopupRedirect = !finalUrl || finalUrl === 'about:blank' || finalUrl.startsWith('chrome-error://');
|
|
3404
|
+
if (!silentMode && !isPopupRedirect) {
|
|
3237
3405
|
console.log(`🔄 Redirect detected: ${originalDomain} → ${finalDomain}`);
|
|
3238
3406
|
}
|
|
3239
3407
|
|
|
@@ -3260,13 +3428,11 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3260
3428
|
}
|
|
3261
3429
|
}
|
|
3262
3430
|
} else {
|
|
3263
|
-
// Invalid final URL
|
|
3264
|
-
console.warn(`⚠ Redirect to invalid URL ignored: ${originalDomain} → ${finalUrl}`);
|
|
3431
|
+
// Invalid final URL (ad popup redirect) - continue with original URL
|
|
3265
3432
|
if (forceDebug) {
|
|
3266
|
-
console.log(formatLogMessage('debug', `
|
|
3433
|
+
console.log(formatLogMessage('debug', `Popup redirect ignored: ${originalDomain} → ${finalUrl}, keeping original: ${originalUrl}`));
|
|
3267
3434
|
}
|
|
3268
|
-
//
|
|
3269
|
-
throw new Error(`Redirect resulted in invalid URL: ${finalUrl}`);
|
|
3435
|
+
// Continue with original URL — requests captured before the redirect are still valid
|
|
3270
3436
|
}
|
|
3271
3437
|
}
|
|
3272
3438
|
}
|
|
@@ -3407,7 +3573,10 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3407
3573
|
const timeoutResult = await handleRedirectTimeout(page, currentUrl, err, safeGetDomain, forceDebug, formatLogMessage);
|
|
3408
3574
|
|
|
3409
3575
|
if (timeoutResult.success) {
|
|
3410
|
-
|
|
3576
|
+
const isPopupRedirect = timeoutResult.finalUrl && (timeoutResult.finalUrl === 'about:blank' || timeoutResult.finalUrl.startsWith('chrome-error://'));
|
|
3577
|
+
if (!isPopupRedirect) {
|
|
3578
|
+
console.log(`⚠ Partial redirect timeout recovered: ${safeGetDomain(currentUrl)} → ${safeGetDomain(timeoutResult.finalUrl)}`);
|
|
3579
|
+
}
|
|
3411
3580
|
currentUrl = timeoutResult.finalUrl; // Use the partial redirect URL
|
|
3412
3581
|
siteCounter++;
|
|
3413
3582
|
// Continue processing with the redirected URL instead of throwing error
|
|
@@ -4036,6 +4205,10 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4036
4205
|
}
|
|
4037
4206
|
}
|
|
4038
4207
|
|
|
4208
|
+
// Track domain timeout counts — skip domain after 3 failures
|
|
4209
|
+
const domainTimeoutCounts = new Map();
|
|
4210
|
+
const DOMAIN_TIMEOUT_THRESHOLD = 3;
|
|
4211
|
+
|
|
4039
4212
|
// Enhanced hang detection with browser restart recovery
|
|
4040
4213
|
let currentBatchInfo = { batchStart: 0, batchSize: 0 };
|
|
4041
4214
|
let lastProcessedCount = 0;
|
|
@@ -4261,8 +4434,17 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4261
4434
|
console.log(formatLogMessage('debug', `[CONCURRENCY] Starting ${batchSize} concurrent tasks with limit ${MAX_CONCURRENT_SITES}`));
|
|
4262
4435
|
}
|
|
4263
4436
|
|
|
4264
|
-
// Create tasks with timeout protection
|
|
4265
|
-
const batchTasks = currentBatch.map(task => originalLimit(() =>
|
|
4437
|
+
// Create tasks with timeout protection — skip domains that repeatedly timed out
|
|
4438
|
+
const batchTasks = currentBatch.map(task => originalLimit(() => {
|
|
4439
|
+
try {
|
|
4440
|
+
const taskDomain = new URL(task.url).hostname;
|
|
4441
|
+
if ((domainTimeoutCounts.get(taskDomain) || 0) >= DOMAIN_TIMEOUT_THRESHOLD) {
|
|
4442
|
+
if (!silentMode) console.log(formatLogMessage('info', `Skipping ${task.url} — ${taskDomain} timed out ${DOMAIN_TIMEOUT_THRESHOLD} times`));
|
|
4443
|
+
return { url: task.url, rules: [], success: false, error: 'Domain repeatedly timed out', skipped: true };
|
|
4444
|
+
}
|
|
4445
|
+
} catch {}
|
|
4446
|
+
return processUrl(task.url, task.config, browser);
|
|
4447
|
+
}));
|
|
4266
4448
|
|
|
4267
4449
|
let batchResults;
|
|
4268
4450
|
try {
|
|
@@ -4292,6 +4474,16 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4292
4474
|
}
|
|
4293
4475
|
}
|
|
4294
4476
|
|
|
4477
|
+
// Track domain timeout counts — skip after threshold
|
|
4478
|
+
for (const result of batchResults) {
|
|
4479
|
+
if (!result.success && !result.skipped && result.error && result.error.includes('timeout')) {
|
|
4480
|
+
try {
|
|
4481
|
+
const domain = new URL(result.url).hostname;
|
|
4482
|
+
domainTimeoutCounts.set(domain, (domainTimeoutCounts.get(domain) || 0) + 1);
|
|
4483
|
+
} catch {}
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4295
4487
|
// IMPROVED: Much more conservative emergency restart logic
|
|
4296
4488
|
const criticalRestartCount = batchResults.filter(r => r.needsImmediateRestart).length;
|
|
4297
4489
|
// Require either:
|
|
@@ -4601,9 +4793,19 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4601
4793
|
// Keep browser open if --keep-open flag is set (useful with --headful for inspection)
|
|
4602
4794
|
if (keepBrowserOpen && !launchHeadless) {
|
|
4603
4795
|
console.log(messageColors.info('Browser kept open.') + ' Close the browser window or press Ctrl+C to exit.');
|
|
4796
|
+
const cleanup = async () => {
|
|
4797
|
+
try {
|
|
4798
|
+
if (browser.isConnected()) await browser.close();
|
|
4799
|
+
} catch {}
|
|
4800
|
+
process.exit(0);
|
|
4801
|
+
};
|
|
4802
|
+
process.on('SIGINT', cleanup);
|
|
4803
|
+
process.on('SIGTERM', cleanup);
|
|
4604
4804
|
await new Promise((resolve) => {
|
|
4605
4805
|
browser.on('disconnected', resolve);
|
|
4606
4806
|
});
|
|
4807
|
+
process.removeListener('SIGINT', cleanup);
|
|
4808
|
+
process.removeListener('SIGTERM', cleanup);
|
|
4607
4809
|
}
|
|
4608
4810
|
|
|
4609
4811
|
// Perform comprehensive final cleanup using enhanced browserexit module
|
|
@@ -4617,15 +4819,23 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4617
4819
|
if (forceDebug) console.log(formatLogMessage('debug', `Browser connection check failed: ${connErr.message}`));
|
|
4618
4820
|
}
|
|
4619
4821
|
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4822
|
+
// Obscura: just disconnect, don't kill — we don't own the browser process
|
|
4823
|
+
let cleanupResult;
|
|
4824
|
+
if (browser._nwssIsObscura) {
|
|
4825
|
+
try { await browser.disconnect(); } catch {}
|
|
4826
|
+
cleanupResult = { success: true, browserClosed: true, tempFilesCleanedCount: 0, userDataCleaned: false, errors: [] };
|
|
4827
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Disconnected from Obscura (process left running)`));
|
|
4828
|
+
} else {
|
|
4829
|
+
cleanupResult = await handleBrowserExit(browser, {
|
|
4830
|
+
forceDebug,
|
|
4831
|
+
timeout: 10000,
|
|
4832
|
+
exitOnFailure: true,
|
|
4833
|
+
cleanTempFiles: true,
|
|
4834
|
+
comprehensiveCleanup: removeTempFiles,
|
|
4835
|
+
userDataDir: browser._nwssUserDataDir,
|
|
4836
|
+
verbose: !silentMode && removeTempFiles
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4629
4839
|
|
|
4630
4840
|
if (forceDebug) {
|
|
4631
4841
|
console.log(formatLogMessage('debug', `Final cleanup results: ${cleanupResult.success ? 'success' : 'failed'}`));
|
|
@@ -4680,6 +4890,24 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
4680
4890
|
if (ignoreCache && forceDebug) {
|
|
4681
4891
|
console.log(messageColors.info('Cache:') + ` Smart caching was disabled`);
|
|
4682
4892
|
}
|
|
4893
|
+
// DNS cache statistics
|
|
4894
|
+
const dnsStats = getDnsCacheStats();
|
|
4895
|
+
if (dnsStats.digHits + dnsStats.digMisses > 0 || dnsStats.whoisHits + dnsStats.whoisMisses > 0) {
|
|
4896
|
+
const parts = [];
|
|
4897
|
+
if (dnsStats.digHits + dnsStats.digMisses > 0) {
|
|
4898
|
+
parts.push(`${messageColors.success(dnsStats.digHits)} dig cached, ${messageColors.timing(dnsStats.digMisses)} fresh`);
|
|
4899
|
+
}
|
|
4900
|
+
if (dnsStats.whoisHits + dnsStats.whoisMisses > 0) {
|
|
4901
|
+
parts.push(`${messageColors.success(dnsStats.whoisHits)} whois cached, ${messageColors.timing(dnsStats.whoisMisses)} fresh`);
|
|
4902
|
+
}
|
|
4903
|
+
console.log(messageColors.info('DNS cache:') + ` ${parts.join(' | ')}`);
|
|
4904
|
+
if (dnsStats.freshDig.length > 0) {
|
|
4905
|
+
console.log(messageColors.info(' Fresh dig:') + ` ${dnsStats.freshDig.join(', ')}`);
|
|
4906
|
+
}
|
|
4907
|
+
if (dnsStats.freshWhois.length > 0) {
|
|
4908
|
+
console.log(messageColors.info(' Fresh whois:') + ` ${dnsStats.freshWhois.join(', ')}`);
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4683
4911
|
}
|
|
4684
4912
|
|
|
4685
4913
|
// Clean process termination
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.61",
|
|
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": {
|