@fanboynz/network-scanner 2.0.31 → 2.0.32
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/domain-cache.js +92 -22
- package/lib/referrer.js +61 -30
- package/nwss.js +2 -2
- package/package.json +1 -1
package/lib/domain-cache.js
CHANGED
|
@@ -10,18 +10,24 @@ const { formatLogMessage } = require('./colorize');
|
|
|
10
10
|
*/
|
|
11
11
|
class DomainCache {
|
|
12
12
|
constructor(options = {}) {
|
|
13
|
+
// V8 Optimization: Initialize all properties in constructor for stable hidden class
|
|
13
14
|
this.cache = new Set();
|
|
15
|
+
|
|
16
|
+
// V8 Optimization: Use consistent object shape (no dynamic property addition)
|
|
14
17
|
this.stats = {
|
|
15
18
|
totalDetected: 0,
|
|
16
19
|
totalSkipped: 0,
|
|
17
20
|
cacheHits: 0,
|
|
18
21
|
cacheMisses: 0
|
|
19
22
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
// V8 Optimization: Store options directly instead of nested object for faster property access
|
|
25
|
+
this.enableLogging = options.enableLogging || false;
|
|
26
|
+
this.logPrefix = options.logPrefix || '[domain-cache]';
|
|
27
|
+
this.maxCacheSize = options.maxCacheSize || 10000; // Prevent memory leaks
|
|
28
|
+
|
|
29
|
+
// V8 Optimization: Pre-calculate 90% target to avoid repeated Math.floor
|
|
30
|
+
this.targetCacheSize = Math.floor(this.maxCacheSize * 0.9);
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/**
|
|
@@ -40,14 +46,14 @@ class DomainCache {
|
|
|
40
46
|
this.stats.totalSkipped++;
|
|
41
47
|
this.stats.cacheHits++;
|
|
42
48
|
|
|
43
|
-
if (this.
|
|
44
|
-
console.log(formatLogMessage('debug', `${this.
|
|
49
|
+
if (this.enableLogging) {
|
|
50
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
|
|
45
51
|
}
|
|
46
52
|
} else {
|
|
47
53
|
this.stats.cacheMisses++;
|
|
48
54
|
|
|
49
|
-
if (this.
|
|
50
|
-
console.log(formatLogMessage('debug', `${this.
|
|
55
|
+
if (this.enableLogging) {
|
|
56
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing)`));
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
@@ -63,25 +69,77 @@ class DomainCache {
|
|
|
63
69
|
return false;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
// Prevent cache from growing too large
|
|
67
|
-
if (this.cache.size >= this.options.maxCacheSize) {
|
|
68
|
-
this.clearOldestEntries(Math.floor(this.options.maxCacheSize * 0.1)); // Remove 10% of entries
|
|
69
|
-
}
|
|
70
|
-
|
|
71
72
|
const wasNew = !this.cache.has(domain);
|
|
72
73
|
this.cache.add(domain);
|
|
73
74
|
|
|
74
75
|
if (wasNew) {
|
|
75
76
|
this.stats.totalDetected++;
|
|
76
77
|
|
|
77
|
-
if (this.
|
|
78
|
-
console.log(formatLogMessage('debug', `${this.
|
|
78
|
+
if (this.enableLogging) {
|
|
79
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Marked as detected: ${domain} (cache size: ${this.cache.size})`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check size AFTER adding to prevent race where multiple threads see same size
|
|
84
|
+
// and all trigger cleanup before any adds complete
|
|
85
|
+
// V8 Optimization: Use pre-calculated targetCacheSize
|
|
86
|
+
if (this.cache.size > this.maxCacheSize) {
|
|
87
|
+
const toRemove = this.cache.size - this.targetCacheSize;
|
|
88
|
+
if (toRemove > 0) {
|
|
89
|
+
this.clearOldestEntries(toRemove);
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
return wasNew;
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Atomically check if domain was detected and mark it if new (race-condition free)
|
|
98
|
+
* This method combines isDomainAlreadyDetected + markDomainAsDetected in one atomic operation
|
|
99
|
+
* @param {string} domain - Domain to check and potentially mark
|
|
100
|
+
* @returns {boolean} True if domain was ALREADY detected (should skip), false if NEW (should process)
|
|
101
|
+
*/
|
|
102
|
+
checkAndMark(domain) {
|
|
103
|
+
if (!domain || typeof domain !== 'string') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const wasAlreadyDetected = this.cache.has(domain);
|
|
108
|
+
|
|
109
|
+
if (wasAlreadyDetected) {
|
|
110
|
+
// Domain already exists - update skip stats and return true (should skip)
|
|
111
|
+
this.stats.totalSkipped++;
|
|
112
|
+
this.stats.cacheHits++;
|
|
113
|
+
|
|
114
|
+
if (this.enableLogging) {
|
|
115
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cache HIT: ${domain} (skipped)`));
|
|
116
|
+
}
|
|
117
|
+
return true; // Already detected, should skip
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Domain is NEW - mark it as detected
|
|
121
|
+
this.stats.cacheMisses++;
|
|
122
|
+
|
|
123
|
+
this.cache.add(domain);
|
|
124
|
+
this.stats.totalDetected++;
|
|
125
|
+
|
|
126
|
+
if (this.enableLogging) {
|
|
127
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cache MISS: ${domain} (processing and marked, cache size: ${this.cache.size})`));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check size AFTER adding to prevent race where multiple threads see same size
|
|
131
|
+
// and all trigger cleanup before any adds complete
|
|
132
|
+
// V8 Optimization: Use pre-calculated targetCacheSize
|
|
133
|
+
if (this.cache.size > this.maxCacheSize) {
|
|
134
|
+
const toRemove = this.cache.size - this.targetCacheSize;
|
|
135
|
+
if (toRemove > 0) {
|
|
136
|
+
this.clearOldestEntries(toRemove);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return false; // New domain, should process
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
/**
|
|
86
144
|
* Clear oldest entries from cache (basic LRU simulation)
|
|
87
145
|
* Note: Set doesn't maintain insertion order in all Node.js versions,
|
|
@@ -96,8 +154,8 @@ class DomainCache {
|
|
|
96
154
|
|
|
97
155
|
toRemove.forEach(domain => this.cache.delete(domain));
|
|
98
156
|
|
|
99
|
-
if (this.
|
|
100
|
-
console.log(formatLogMessage('debug', `${this.
|
|
157
|
+
if (this.enableLogging) {
|
|
158
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cleared ${toRemove.length} old entries, cache size now: ${this.cache.size}`));
|
|
101
159
|
}
|
|
102
160
|
}
|
|
103
161
|
|
|
@@ -128,8 +186,8 @@ class DomainCache {
|
|
|
128
186
|
cacheMisses: 0
|
|
129
187
|
};
|
|
130
188
|
|
|
131
|
-
if (this.
|
|
132
|
-
console.log(formatLogMessage('debug', `${this.
|
|
189
|
+
if (this.enableLogging) {
|
|
190
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Cache cleared (${previousSize} entries removed)`));
|
|
133
191
|
}
|
|
134
192
|
}
|
|
135
193
|
|
|
@@ -158,8 +216,8 @@ class DomainCache {
|
|
|
158
216
|
removeDomain(domain) {
|
|
159
217
|
const wasRemoved = this.cache.delete(domain);
|
|
160
218
|
|
|
161
|
-
if (wasRemoved && this.
|
|
162
|
-
console.log(formatLogMessage('debug', `${this.
|
|
219
|
+
if (wasRemoved && this.enableLogging) {
|
|
220
|
+
console.log(formatLogMessage('debug', `${this.logPrefix} Removed from cache: ${domain}`));
|
|
163
221
|
}
|
|
164
222
|
|
|
165
223
|
return wasRemoved;
|
|
@@ -193,6 +251,7 @@ class DomainCache {
|
|
|
193
251
|
return {
|
|
194
252
|
isDomainAlreadyDetected: this.isDomainAlreadyDetected.bind(this),
|
|
195
253
|
markDomainAsDetected: this.markDomainAsDetected.bind(this),
|
|
254
|
+
checkAndMark: this.checkAndMark.bind(this),
|
|
196
255
|
getSkippedCount: () => this.stats.totalSkipped,
|
|
197
256
|
getCacheSize: () => this.cache.size,
|
|
198
257
|
getStats: this.getStats.bind(this)
|
|
@@ -261,6 +320,16 @@ function markDomainAsDetected(domain) {
|
|
|
261
320
|
cache.markDomainAsDetected(domain);
|
|
262
321
|
}
|
|
263
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Atomically check and mark a domain (race-condition free)
|
|
325
|
+
* @param {string} domain - Domain to check and mark
|
|
326
|
+
* @returns {boolean} True if already detected (skip), false if new (process)
|
|
327
|
+
*/
|
|
328
|
+
function checkAndMark(domain) {
|
|
329
|
+
const cache = getGlobalDomainCache();
|
|
330
|
+
return cache.checkAndMark(domain);
|
|
331
|
+
}
|
|
332
|
+
|
|
264
333
|
/**
|
|
265
334
|
* Get total domains skipped (legacy wrapper)
|
|
266
335
|
* @returns {number} Number of domains skipped
|
|
@@ -291,6 +360,7 @@ module.exports = {
|
|
|
291
360
|
// Legacy wrapper functions for backward compatibility
|
|
292
361
|
isDomainAlreadyDetected,
|
|
293
362
|
markDomainAsDetected,
|
|
363
|
+
checkAndMark,
|
|
294
364
|
getTotalDomainsSkipped,
|
|
295
365
|
getDetectedDomainsCount
|
|
296
366
|
};
|
package/lib/referrer.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// === Referrer Header Generation Module ===
|
|
2
2
|
// This module handles generation of referrer headers for different traffic simulation modes
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Performance utility: Get random element from array
|
|
6
|
+
* Reduces code duplication and improves readability
|
|
7
|
+
* @param {Array} array - Array to select from
|
|
8
|
+
* @returns {*} Random element from array
|
|
9
|
+
*/
|
|
10
|
+
function getRandomElement(array) {
|
|
11
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* Referrer URL collections for different modes
|
|
6
16
|
*/
|
|
@@ -69,7 +79,7 @@ const REFERRER_COLLECTIONS = Object.freeze({
|
|
|
69
79
|
*/
|
|
70
80
|
function generateSearchTerm(customTerms, context = null) {
|
|
71
81
|
if (customTerms && customTerms.length > 0) {
|
|
72
|
-
return
|
|
82
|
+
return getRandomElement(customTerms);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
// Use context-specific terms if available
|
|
@@ -80,7 +90,7 @@ function generateSearchTerm(customTerms, context = null) {
|
|
|
80
90
|
termCollection = REFERRER_COLLECTIONS.TECH_TERMS;
|
|
81
91
|
}
|
|
82
92
|
|
|
83
|
-
return
|
|
93
|
+
return getRandomElement(termCollection);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/**
|
|
@@ -91,9 +101,8 @@ function generateSearchTerm(customTerms, context = null) {
|
|
|
91
101
|
* @returns {string} Generated search engine referrer URL
|
|
92
102
|
*/
|
|
93
103
|
function generateSearchReferrer(searchTerms, context, forceDebug) {
|
|
94
|
-
const randomEngine = REFERRER_COLLECTIONS.SEARCH_ENGINES
|
|
95
|
-
|
|
96
|
-
];
|
|
104
|
+
const randomEngine = getRandomElement(REFERRER_COLLECTIONS.SEARCH_ENGINES);
|
|
105
|
+
|
|
97
106
|
const searchTerm = generateSearchTerm(searchTerms, context);
|
|
98
107
|
const referrerUrl = randomEngine + encodeURIComponent(searchTerm);
|
|
99
108
|
|
|
@@ -110,9 +119,7 @@ function generateSearchReferrer(searchTerms, context, forceDebug) {
|
|
|
110
119
|
* @returns {string} Generated social media referrer URL
|
|
111
120
|
*/
|
|
112
121
|
function generateSocialMediaReferrer(forceDebug) {
|
|
113
|
-
const randomSocial = REFERRER_COLLECTIONS.SOCIAL_MEDIA
|
|
114
|
-
Math.floor(Math.random() * REFERRER_COLLECTIONS.SOCIAL_MEDIA.length)
|
|
115
|
-
];
|
|
122
|
+
const randomSocial = getRandomElement(REFERRER_COLLECTIONS.SOCIAL_MEDIA);
|
|
116
123
|
|
|
117
124
|
if (forceDebug) {
|
|
118
125
|
console.log(`[debug] Generated social media referrer: ${randomSocial}`);
|
|
@@ -127,9 +134,7 @@ function generateSocialMediaReferrer(forceDebug) {
|
|
|
127
134
|
* @returns {string} Generated news site referrer URL
|
|
128
135
|
*/
|
|
129
136
|
function generateNewsReferrer(forceDebug) {
|
|
130
|
-
const randomNews = REFERRER_COLLECTIONS.NEWS_SITES
|
|
131
|
-
Math.floor(Math.random() * REFERRER_COLLECTIONS.NEWS_SITES.length)
|
|
132
|
-
];
|
|
137
|
+
const randomNews = getRandomElement(REFERRER_COLLECTIONS.NEWS_SITES);
|
|
133
138
|
|
|
134
139
|
if (forceDebug) {
|
|
135
140
|
console.log(`[debug] Generated news referrer: ${randomNews}`);
|
|
@@ -155,33 +160,47 @@ function isValidUrl(url) {
|
|
|
155
160
|
* @returns {boolean} True if referrer should be disabled for this URL
|
|
156
161
|
*/
|
|
157
162
|
function shouldDisableReferrer(targetUrl, disableList, forceDebug = false) {
|
|
158
|
-
|
|
163
|
+
// Fast path: early return for empty/invalid inputs
|
|
164
|
+
if (!disableList?.length || !targetUrl || typeof targetUrl !== 'string') {
|
|
159
165
|
return false;
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
|
|
163
|
-
|
|
168
|
+
// Parse target URL once (performance optimization)
|
|
169
|
+
let targetHostname = null;
|
|
170
|
+
let targetUrlParsed = false;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
targetHostname = new URL(targetUrl).hostname;
|
|
174
|
+
targetUrlParsed = true;
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Invalid URL - can only do string matching
|
|
177
|
+
targetUrlParsed = false;
|
|
164
178
|
}
|
|
165
179
|
|
|
166
180
|
for (const disablePattern of disableList) {
|
|
167
181
|
if (typeof disablePattern !== 'string') continue;
|
|
168
182
|
|
|
169
|
-
// Exact URL match
|
|
183
|
+
// Fast check: Exact URL match (no parsing needed)
|
|
170
184
|
if (targetUrl === disablePattern) {
|
|
171
|
-
if (forceDebug) console.log(`[debug] Referrer disabled for exact
|
|
185
|
+
if (forceDebug) console.log(`[debug] Referrer disabled for exact match: ${targetUrl}`);
|
|
172
186
|
return true;
|
|
173
187
|
}
|
|
174
188
|
|
|
175
|
-
// Domain/hostname match
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
// Domain/hostname match (use cached parsed URL)
|
|
190
|
+
if (targetUrlParsed) {
|
|
191
|
+
try {
|
|
192
|
+
const disableHostname = new URL(disablePattern).hostname;
|
|
193
|
+
if (targetHostname === disableHostname) {
|
|
194
|
+
if (forceDebug) console.log(`[debug] Referrer disabled for domain match: ${targetHostname}`);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// disablePattern is not a valid URL, try substring match below
|
|
182
199
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Fallback: Simple substring match (for patterns like 'example.com')
|
|
203
|
+
if (!targetUrlParsed || disablePattern.includes('/') === false) {
|
|
185
204
|
if (targetUrl.includes(disablePattern)) {
|
|
186
205
|
if (forceDebug) console.log(`[debug] Referrer disabled for pattern match: ${disablePattern} in ${targetUrl}`);
|
|
187
206
|
return true;
|
|
@@ -218,7 +237,7 @@ function generateReferrerUrl(referrerConfig, forceDebug = false) {
|
|
|
218
237
|
return '';
|
|
219
238
|
}
|
|
220
239
|
|
|
221
|
-
const randomUrl =
|
|
240
|
+
const randomUrl = getRandomElement(referrerConfig);
|
|
222
241
|
const url = isValidUrl(randomUrl) ? randomUrl : '';
|
|
223
242
|
|
|
224
243
|
if (forceDebug) {
|
|
@@ -261,7 +280,7 @@ function generateReferrerUrl(referrerConfig, forceDebug = false) {
|
|
|
261
280
|
case 'mixed': {
|
|
262
281
|
// Randomly choose between different referrer types
|
|
263
282
|
const modes = ['random_search', 'social_media', 'news_sites'];
|
|
264
|
-
const randomMode =
|
|
283
|
+
const randomMode = getRandomElement(modes);
|
|
265
284
|
|
|
266
285
|
if (forceDebug) console.log(`[debug] Mixed mode selected: ${randomMode}`);
|
|
267
286
|
|
|
@@ -335,14 +354,26 @@ function validateReferrerConfig(referrerConfig) {
|
|
|
335
354
|
if (Array.isArray(referrerConfig)) {
|
|
336
355
|
if (referrerConfig.length === 0) {
|
|
337
356
|
result.warnings.push('Empty referrer array will result in no referrer');
|
|
338
|
-
|
|
339
|
-
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fast validation: check only first and last items if array is large
|
|
361
|
+
const itemsToCheck = referrerConfig.length > 10
|
|
362
|
+
? [referrerConfig[0], referrerConfig[referrerConfig.length - 1]]
|
|
363
|
+
: referrerConfig;
|
|
364
|
+
|
|
365
|
+
itemsToCheck.forEach((url, index) => {
|
|
340
366
|
if (!isValidUrl(url)) {
|
|
341
|
-
|
|
367
|
+
const actualIndex = itemsToCheck === referrerConfig ? index : (index === 0 ? 0 : referrerConfig.length - 1);
|
|
368
|
+
result.errors.push(`Array item ${actualIndex} is not a valid HTTP/HTTPS URL: ${url}`);
|
|
342
369
|
result.isValid = false;
|
|
343
370
|
}
|
|
344
371
|
});
|
|
372
|
+
|
|
373
|
+
if (referrerConfig.length > 10 && itemsToCheck.length < referrerConfig.length) {
|
|
374
|
+
result.warnings.push(`Large array (${referrerConfig.length} items): only validated first and last items for performance`);
|
|
345
375
|
}
|
|
376
|
+
|
|
346
377
|
return result;
|
|
347
378
|
}
|
|
348
379
|
|
package/nwss.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// === Network scanner script (nwss.js) v2.0.
|
|
1
|
+
// === Network scanner script (nwss.js) v2.0.32 ===
|
|
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
|
|
@@ -145,7 +145,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
|
|
|
145
145
|
const { monitorBrowserHealth, isBrowserHealthy, isQuicklyResponsive, performGroupWindowCleanup, performRealtimeWindowCleanup, trackPageForRealtime, updatePageUsage, cleanupPageBeforeReload } = require('./lib/browserhealth');
|
|
146
146
|
|
|
147
147
|
// --- Script Configuration & Constants ---
|
|
148
|
-
const VERSION = '2.0.
|
|
148
|
+
const VERSION = '2.0.32'; // Script version
|
|
149
149
|
|
|
150
150
|
// get startTime
|
|
151
151
|
const startTime = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.32",
|
|
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": {
|