@fanboynz/network-scanner 2.0.66 → 3.0.0
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/.github/workflows/npm-publish.yml +134 -10
- package/CHANGELOG.md +135 -0
- package/CLAUDE.md +18 -7
- package/README.md +12 -4
- package/lib/adblock-rust.js +23 -18
- package/lib/adblock.js +127 -82
- package/lib/browserexit.js +210 -200
- package/lib/browserhealth.js +84 -60
- package/lib/cdp.js +103 -81
- package/lib/clear_sitedata.js +61 -159
- package/lib/cloudflare.js +579 -409
- package/lib/colorize.js +29 -12
- package/lib/compare.js +16 -8
- package/lib/compress.js +2 -1
- package/lib/curl.js +287 -220
- package/lib/domain-cache.js +87 -40
- package/lib/dry-run.js +137 -194
- package/lib/fingerprint.js +20 -18
- package/lib/flowproxy.js +391 -188
- package/lib/ghost-cursor.js +8 -7
- package/lib/grep.js +248 -171
- package/lib/ignore_similar.js +70 -124
- package/lib/interaction.js +132 -235
- package/lib/nettools.js +309 -87
- package/lib/openvpn_vpn.js +12 -11
- package/lib/output.js +92 -59
- package/lib/post-processing.js +216 -162
- package/lib/redirect.js +46 -30
- package/lib/referrer.js +158 -165
- package/lib/searchstring.js +290 -381
- package/lib/smart-cache.js +141 -91
- package/lib/socks-relay.js +8 -7
- package/lib/spawn-async.js +137 -0
- package/lib/validate_rules.js +188 -176
- package/lib/wireguard_vpn.js +111 -117
- package/nwss.js +740 -156
- package/package.json +4 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// === Shared async-spawn helper ===
|
|
2
|
+
// Single canonical implementation of "spawn an external command, collect
|
|
3
|
+
// stdout/stderr with hard caps, enforce a kill timeout" — extracted from
|
|
4
|
+
// what used to be ~400 lines of near-identical Promise wrappers across
|
|
5
|
+
// lib/curl.js, lib/searchstring.js, and lib/grep.js (two sites in the
|
|
6
|
+
// latter). Each caller previously had its own copy of the same pattern:
|
|
7
|
+
// truncate-on-cap, SIGKILL belt-and-braces timer, stdout buffer concat,
|
|
8
|
+
// error/close handlers.
|
|
9
|
+
//
|
|
10
|
+
// Design notes:
|
|
11
|
+
// - Resolves (never rejects). Callers inspect the result object to
|
|
12
|
+
// distinguish failure modes instead of needing try/catch. Pre-existing
|
|
13
|
+
// call sites all converted failure into a result-object anyway —
|
|
14
|
+
// this just standardizes the shape.
|
|
15
|
+
// - stdout/stderr returned as Buffer so the caller decides decoding
|
|
16
|
+
// (curl's `--write-out` metadata is line-oriented and tolerant of
|
|
17
|
+
// UTF-8 decoding mid-multibyte; binary HTTP responses are not).
|
|
18
|
+
// - lib/wireguard_vpn.js intentionally stays on spawnSync — its calls
|
|
19
|
+
// are startup-only (validation, health check) and the synchronous
|
|
20
|
+
// semantics are simpler. Not all spawn calls benefit from async.
|
|
21
|
+
|
|
22
|
+
const { spawn } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
25
|
+
const DEFAULT_MAX_STDOUT = 50 * 1024 * 1024; // 50MB
|
|
26
|
+
const KILL_GRACE_MS = 5000; // belt-and-braces SIGKILL after timeout+grace
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Spawn a process asynchronously, collect stdout/stderr with hard caps,
|
|
30
|
+
* enforce a kill timeout. Resolves with a result object describing what
|
|
31
|
+
* happened — never rejects.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} cmd - Executable name (no shell — args are not parsed)
|
|
34
|
+
* @param {string[]} args - Argument array
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @param {number} [opts.timeout=30000] - Soft timeout in ms; primary
|
|
37
|
+
* limit is whatever the executable itself enforces (e.g. curl
|
|
38
|
+
* --max-time). This is the belt-and-braces SIGKILL deadline fired at
|
|
39
|
+
* `timeout + 5000` ms.
|
|
40
|
+
* @param {number} [opts.maxStdout=52428800] - Cap on stdout collection.
|
|
41
|
+
* When the cap is exceeded the child is killed with SIGTERM and the
|
|
42
|
+
* result has `truncated: true`.
|
|
43
|
+
* @param {string|Buffer} [opts.input] - Data to write to the child's
|
|
44
|
+
* stdin then close. EPIPE on stdin is swallowed (child may exit early).
|
|
45
|
+
* @param {boolean} [opts.collectStderr=true] - When false, stderr is
|
|
46
|
+
* drained but not retained (saves memory when caller doesn't need it).
|
|
47
|
+
* @returns {Promise<{
|
|
48
|
+
* code: number|null,
|
|
49
|
+
* signal: string|null,
|
|
50
|
+
* stdout: Buffer,
|
|
51
|
+
* stderr: Buffer,
|
|
52
|
+
* truncated: boolean,
|
|
53
|
+
* error: string|null
|
|
54
|
+
* }>}
|
|
55
|
+
*/
|
|
56
|
+
function runProcess(cmd, args, opts = {}) {
|
|
57
|
+
const {
|
|
58
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
59
|
+
maxStdout = DEFAULT_MAX_STDOUT,
|
|
60
|
+
input,
|
|
61
|
+
collectStderr = true
|
|
62
|
+
} = opts;
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
let child;
|
|
66
|
+
try {
|
|
67
|
+
child = spawn(cmd, args);
|
|
68
|
+
} catch (spawnErr) {
|
|
69
|
+
resolve({
|
|
70
|
+
code: null, signal: null,
|
|
71
|
+
stdout: Buffer.alloc(0), stderr: Buffer.alloc(0),
|
|
72
|
+
truncated: false, error: spawnErr.message
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stdoutChunks = [];
|
|
78
|
+
const stderrChunks = [];
|
|
79
|
+
let stdoutBytes = 0;
|
|
80
|
+
let truncated = false;
|
|
81
|
+
|
|
82
|
+
child.stdout.on('data', (chunk) => {
|
|
83
|
+
if (truncated) return;
|
|
84
|
+
if (stdoutBytes + chunk.length > maxStdout) {
|
|
85
|
+
truncated = true;
|
|
86
|
+
try { child.kill('SIGTERM'); } catch (_) {}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
stdoutBytes += chunk.length;
|
|
90
|
+
stdoutChunks.push(chunk);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (collectStderr) {
|
|
94
|
+
child.stderr.on('data', (chunk) => { stderrChunks.push(chunk); });
|
|
95
|
+
} else {
|
|
96
|
+
child.stderr.on('data', () => {}); // drain but discard
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// SIGKILL belt-and-braces after timeout+grace. unref'd so the timer
|
|
100
|
+
// doesn't keep the event loop alive on its own; if the process exits
|
|
101
|
+
// earlier, the timer is cleared in the close handler.
|
|
102
|
+
const killTimer = setTimeout(() => {
|
|
103
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
104
|
+
}, timeout + KILL_GRACE_MS);
|
|
105
|
+
if (typeof killTimer.unref === 'function') killTimer.unref();
|
|
106
|
+
|
|
107
|
+
child.on('error', (err) => {
|
|
108
|
+
clearTimeout(killTimer);
|
|
109
|
+
resolve({
|
|
110
|
+
code: null, signal: null,
|
|
111
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
112
|
+
stderr: Buffer.concat(stderrChunks),
|
|
113
|
+
truncated, error: err.message
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
child.on('close', (code, signal) => {
|
|
118
|
+
clearTimeout(killTimer);
|
|
119
|
+
resolve({
|
|
120
|
+
code, signal,
|
|
121
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
122
|
+
stderr: Buffer.concat(stderrChunks),
|
|
123
|
+
truncated, error: null
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (input !== undefined) {
|
|
128
|
+
// EPIPE if the child exited before we finished writing (e.g. grep
|
|
129
|
+
// matched and bailed early, or our truncation kill fired). Swallow
|
|
130
|
+
// so it doesn't surface as an unhandled stream error.
|
|
131
|
+
child.stdin.on('error', () => {});
|
|
132
|
+
child.stdin.end(input);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { runProcess };
|
package/lib/validate_rules.js
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
const
|
|
1
|
+
const net = require('node:net');
|
|
2
|
+
const { formatLogMessage, messageColors } = require('./colorize');
|
|
3
|
+
// Cross-module validators wired into site-config validation — previously
|
|
4
|
+
// each had to be called separately (or wasn't called at all). Centralizing
|
|
5
|
+
// here means a single validateSiteConfig surfaces ALL misconfigurations
|
|
6
|
+
// at startup instead of mid-scan.
|
|
7
|
+
// - validateSearchString: had ZERO callers anywhere before this hookup.
|
|
8
|
+
// - validateVpnConfig / validateOvpnConfig: called inside connectForSite
|
|
9
|
+
// per-site at scan time. Adding here catches errors at startup.
|
|
10
|
+
const { validateSearchString } = require('./searchstring');
|
|
11
|
+
const { validateVpnConfig: validateWgConfig, normalizeVpnConfig: normalizeWgConfig } = require('./wireguard_vpn');
|
|
12
|
+
const { validateOvpnConfig, normalizeOvpnConfig } = require('./openvpn_vpn');
|
|
13
|
+
const CLEAN_TAG = messageColors.processing('[clean]');
|
|
2
14
|
|
|
3
|
-
// Pre-compiled regex constants for validation
|
|
15
|
+
// Pre-compiled regex constants for validation. IPv4/IPv6 validation now
|
|
16
|
+
// uses Node's built-in net.isIP() — the old hand-rolled regexes were
|
|
17
|
+
// incomplete (missing IPv4-mapped IPv6, zone identifiers, etc.) and
|
|
18
|
+
// silently accepted some malformed inputs like '2001:db8:::1'.
|
|
4
19
|
const REGEX_LABEL = /^[a-zA-Z0-9-]+$/;
|
|
5
20
|
const REGEX_TLD = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
6
|
-
|
|
7
|
-
|
|
21
|
+
|
|
22
|
+
// Module-level Set of valid adblock filter modifiers. Was previously
|
|
23
|
+
// re-allocated inside validateAdblockModifiers on every call — for a
|
|
24
|
+
// 100k-line filter list that's 100k identical Set allocations.
|
|
25
|
+
const VALID_MODIFIERS = new Set([
|
|
26
|
+
// Resource type modifiers
|
|
27
|
+
'script', 'stylesheet', 'image', 'object', 'xmlhttprequest', 'subdocument',
|
|
28
|
+
'ping', 'websocket', 'webrtc', 'document', 'elemhide', 'generichide',
|
|
29
|
+
'genericblock', 'popup', 'font', 'media', 'other',
|
|
30
|
+
// Party modifiers
|
|
31
|
+
'third-party', 'first-party', '~third-party', '~first-party',
|
|
32
|
+
// Domain modifiers (domain= is validated separately below)
|
|
33
|
+
'domain',
|
|
34
|
+
// Method modifiers
|
|
35
|
+
'match-case', '~match-case',
|
|
36
|
+
// Action modifiers
|
|
37
|
+
'important', 'badfilter',
|
|
38
|
+
// CSP and redirect modifiers
|
|
39
|
+
'csp', 'redirect', 'redirect-rule',
|
|
40
|
+
// uBlock Origin specific
|
|
41
|
+
'inline-script', 'inline-font', 'mp4', 'empty', 'xhr'
|
|
42
|
+
]);
|
|
8
43
|
|
|
9
44
|
/**
|
|
10
45
|
* Enhanced domain validation function
|
|
@@ -128,30 +163,31 @@ function isValidTLD(tld) {
|
|
|
128
163
|
}
|
|
129
164
|
|
|
130
165
|
/**
|
|
131
|
-
* Checks if a string is an IP address (IPv4 or IPv6)
|
|
166
|
+
* Checks if a string is an IP address (IPv4 or IPv6).
|
|
167
|
+
* Delegates to Node's net.isIP() — standards-compliant, no regex to
|
|
168
|
+
* maintain. Returns true for any valid IP form including IPv4-mapped
|
|
169
|
+
* IPv6 (::ffff:192.0.2.1) which the old hand-rolled regex missed.
|
|
132
170
|
* @param {string} str - String to check
|
|
133
171
|
* @returns {boolean} True if it's an IP address
|
|
134
172
|
*/
|
|
135
173
|
function isIPAddress(str) {
|
|
136
|
-
return
|
|
174
|
+
return net.isIP(str) !== 0;
|
|
137
175
|
}
|
|
138
176
|
|
|
139
177
|
/**
|
|
140
|
-
*
|
|
141
|
-
* @param {string} str - String to check
|
|
178
|
+
* @param {string} str
|
|
142
179
|
* @returns {boolean} True if valid IPv4
|
|
143
180
|
*/
|
|
144
181
|
function isIPv4(str) {
|
|
145
|
-
return
|
|
182
|
+
return net.isIPv4(str);
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
/**
|
|
149
|
-
*
|
|
150
|
-
* @param {string} str - String to check
|
|
186
|
+
* @param {string} str
|
|
151
187
|
* @returns {boolean} True if valid IPv6
|
|
152
188
|
*/
|
|
153
189
|
function isIPv6(str) {
|
|
154
|
-
return
|
|
190
|
+
return net.isIPv6(str);
|
|
155
191
|
}
|
|
156
192
|
|
|
157
193
|
/**
|
|
@@ -163,11 +199,19 @@ function validateRegexPattern(pattern) {
|
|
|
163
199
|
if (!pattern || typeof pattern !== 'string') {
|
|
164
200
|
return { isValid: false, error: 'Pattern must be a non-empty string' };
|
|
165
201
|
}
|
|
166
|
-
|
|
202
|
+
|
|
167
203
|
try {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
new RegExp(
|
|
204
|
+
// Handle /pattern/flags literal syntax. The old `^\/(.*)\/$/` strip
|
|
205
|
+
// didn't match patterns with flags ('/foo/i'), so they passed through
|
|
206
|
+
// unchanged to `new RegExp('/foo/i')` — which compiled a regex that
|
|
207
|
+
// matched the LITERAL string '/foo/i' instead of the intended `foo`
|
|
208
|
+
// pattern with the `i` flag. Silent acceptance of malformed input.
|
|
209
|
+
const literalMatch = pattern.match(/^\/(.*)\/([gimsuy]*)$/);
|
|
210
|
+
if (literalMatch) {
|
|
211
|
+
new RegExp(literalMatch[1], literalMatch[2]);
|
|
212
|
+
} else {
|
|
213
|
+
new RegExp(pattern);
|
|
214
|
+
}
|
|
171
215
|
return { isValid: true };
|
|
172
216
|
} catch (err) {
|
|
173
217
|
return { isValid: false, error: `Invalid regex: ${err.message}` };
|
|
@@ -183,33 +227,7 @@ function validateAdblockModifiers(modifiers) {
|
|
|
183
227
|
if (!modifiers) {
|
|
184
228
|
return { isValid: true, modifiers: [] };
|
|
185
229
|
}
|
|
186
|
-
|
|
187
|
-
// Valid adblock filter modifiers
|
|
188
|
-
const validModifiers = new Set([
|
|
189
|
-
// Resource type modifiers
|
|
190
|
-
'script', 'stylesheet', 'image', 'object', 'xmlhttprequest', 'subdocument',
|
|
191
|
-
'ping', 'websocket', 'webrtc', 'document', 'elemhide', 'generichide',
|
|
192
|
-
'genericblock', 'popup', 'font', 'media', 'other',
|
|
193
|
-
|
|
194
|
-
// Party modifiers
|
|
195
|
-
'third-party', 'first-party', '~third-party', '~first-party',
|
|
196
|
-
|
|
197
|
-
// Domain modifiers (domain= will be validated separately)
|
|
198
|
-
'domain',
|
|
199
|
-
|
|
200
|
-
// Method modifiers
|
|
201
|
-
'match-case', '~match-case',
|
|
202
|
-
|
|
203
|
-
// Action modifiers
|
|
204
|
-
'important', 'badfilter',
|
|
205
|
-
|
|
206
|
-
// CSP and redirect modifiers
|
|
207
|
-
'csp', 'redirect', 'redirect-rule',
|
|
208
|
-
|
|
209
|
-
// uBlock Origin specific
|
|
210
|
-
'inline-script', 'inline-font', 'mp4', 'empty', 'xhr'
|
|
211
|
-
]);
|
|
212
|
-
|
|
230
|
+
|
|
213
231
|
const modifierList = modifiers.split(',').map(m => m.trim());
|
|
214
232
|
const invalidModifiers = [];
|
|
215
233
|
const parsedModifiers = [];
|
|
@@ -262,7 +280,7 @@ function validateAdblockModifiers(modifiers) {
|
|
|
262
280
|
const isNegated = modifier.startsWith('~');
|
|
263
281
|
const baseModifier = isNegated ? modifier.substring(1) : modifier;
|
|
264
282
|
|
|
265
|
-
if (
|
|
283
|
+
if (VALID_MODIFIERS.has(modifier) || VALID_MODIFIERS.has(baseModifier)) {
|
|
266
284
|
parsedModifiers.push({
|
|
267
285
|
type: baseModifier,
|
|
268
286
|
negated: isNegated,
|
|
@@ -296,91 +314,124 @@ function validateAdblockRule(rule) {
|
|
|
296
314
|
if (!rule || typeof rule !== 'string') {
|
|
297
315
|
return { isValid: false, format: 'unknown', error: 'Rule must be a non-empty string' };
|
|
298
316
|
}
|
|
299
|
-
|
|
317
|
+
|
|
300
318
|
const trimmedRule = rule.trim();
|
|
301
|
-
|
|
319
|
+
|
|
302
320
|
// Skip comments
|
|
303
321
|
if (trimmedRule.startsWith('!') || trimmedRule.startsWith('#')) {
|
|
304
322
|
return { isValid: true, format: 'comment' };
|
|
305
323
|
}
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
324
|
+
|
|
325
|
+
// Strip @@ exception (whitelist) prefix and run the rest of validation
|
|
326
|
+
// on the remainder. Exception rules are standard adblock syntax
|
|
327
|
+
// (e.g. '@@||example.com^', '@@||example.com^$image') and appear
|
|
328
|
+
// throughout real-world filter lists like EasyList — without this,
|
|
329
|
+
// `nwss --validate-rules easylist.txt` flagged every exception as
|
|
330
|
+
// 'Unrecognized rule format'. We attach `isException: true` to the
|
|
331
|
+
// result so downstream consumers can see the whitelist intent.
|
|
332
|
+
let isException = false;
|
|
333
|
+
let working = trimmedRule;
|
|
334
|
+
if (working.startsWith('@@')) {
|
|
335
|
+
isException = true;
|
|
336
|
+
working = working.substring(2);
|
|
337
|
+
if (!working) {
|
|
338
|
+
return { isValid: false, format: 'unknown', error: '@@ exception prefix with no rule body' };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// @@ only makes sense as a prefix for adblock-format rules. Bail
|
|
343
|
+
// early if it's prefixing something else (e.g. '@@127.0.0.1 host'
|
|
344
|
+
// is meaningless — localhost format has no exception concept).
|
|
345
|
+
if (isException &&
|
|
346
|
+
!(working.startsWith('||') && working.includes('^')) &&
|
|
347
|
+
!(working.includes('^$'))) {
|
|
348
|
+
return {
|
|
349
|
+
isValid: false,
|
|
350
|
+
format: 'unknown',
|
|
351
|
+
isException: true,
|
|
352
|
+
error: '@@ exception prefix only valid for adblock-format rules'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Adblock format: ||domain.com^ or ||domain.com^$script,third-party.
|
|
357
|
+
// Uses `working` (post-@@-strip body) instead of `trimmedRule`.
|
|
358
|
+
const ruleBody = working;
|
|
359
|
+
if (ruleBody.startsWith('||') && ruleBody.includes('^')) {
|
|
360
|
+
const parts = ruleBody.substring(2).split('^');
|
|
310
361
|
const domain = parts[0];
|
|
311
|
-
|
|
362
|
+
|
|
312
363
|
if (!isValidDomain(domain)) {
|
|
313
|
-
return { isValid: false, format: 'adblock', error: `Invalid domain in adblock rule: ${domain}` };
|
|
364
|
+
return { isValid: false, format: 'adblock', isException, error: `Invalid domain in adblock rule: ${domain}` };
|
|
314
365
|
}
|
|
315
|
-
|
|
366
|
+
|
|
316
367
|
// Check for modifiers after ^$
|
|
317
368
|
let modifiers = '';
|
|
318
369
|
let modifierValidation = { isValid: true, modifiers: [] };
|
|
319
|
-
|
|
370
|
+
|
|
320
371
|
if (parts.length > 1 && parts[1].startsWith('$')) {
|
|
321
372
|
modifiers = parts[1].substring(1);
|
|
322
373
|
modifierValidation = validateAdblockModifiers(modifiers);
|
|
323
|
-
|
|
374
|
+
|
|
324
375
|
if (!modifierValidation.isValid) {
|
|
325
|
-
return {
|
|
326
|
-
isValid: false,
|
|
327
|
-
format: 'adblock',
|
|
376
|
+
return {
|
|
377
|
+
isValid: false,
|
|
378
|
+
format: 'adblock',
|
|
379
|
+
isException,
|
|
328
380
|
error: `${modifierValidation.error} in rule: ${trimmedRule}`,
|
|
329
381
|
domain,
|
|
330
382
|
modifiers: modifierValidation.validModifiers || []
|
|
331
383
|
};
|
|
332
384
|
}
|
|
333
385
|
}
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
isValid: true,
|
|
337
|
-
format: 'adblock',
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
isValid: true,
|
|
389
|
+
format: 'adblock',
|
|
390
|
+
isException,
|
|
338
391
|
domain,
|
|
339
392
|
modifiers: modifierValidation.modifiers,
|
|
340
393
|
hasModifiers: modifiers.length > 0
|
|
341
394
|
};
|
|
342
395
|
}
|
|
343
|
-
|
|
396
|
+
|
|
344
397
|
// Basic adblock format without ||: domain.com^$modifier
|
|
345
|
-
if (
|
|
346
|
-
const parts =
|
|
398
|
+
if (ruleBody.includes('^') && ruleBody.includes('$')) {
|
|
399
|
+
const parts = ruleBody.split('^$');
|
|
347
400
|
if (parts.length === 2) {
|
|
348
401
|
const domain = parts[0];
|
|
349
402
|
const modifiers = parts[1];
|
|
350
|
-
|
|
403
|
+
|
|
351
404
|
if (!isValidDomain(domain)) {
|
|
352
|
-
return { isValid: false, format: 'adblock-basic', error: `Invalid domain in adblock rule: ${domain}` };
|
|
405
|
+
return { isValid: false, format: 'adblock-basic', isException, error: `Invalid domain in adblock rule: ${domain}` };
|
|
353
406
|
}
|
|
354
|
-
|
|
407
|
+
|
|
355
408
|
const modifierValidation = validateAdblockModifiers(modifiers);
|
|
356
409
|
if (!modifierValidation.isValid) {
|
|
357
|
-
return {
|
|
358
|
-
isValid: false,
|
|
359
|
-
format: 'adblock-basic',
|
|
410
|
+
return {
|
|
411
|
+
isValid: false,
|
|
412
|
+
format: 'adblock-basic',
|
|
413
|
+
isException,
|
|
360
414
|
error: modifierValidation.error,
|
|
361
415
|
domain
|
|
362
416
|
};
|
|
363
417
|
}
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
isValid: true,
|
|
367
|
-
format: 'adblock-basic',
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
isValid: true,
|
|
421
|
+
format: 'adblock-basic',
|
|
422
|
+
isException,
|
|
368
423
|
domain,
|
|
369
424
|
modifiers: modifierValidation.modifiers
|
|
370
425
|
};
|
|
371
426
|
}
|
|
372
427
|
}
|
|
373
428
|
|
|
374
|
-
// Simple adblock format
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return { isValid: false, format: 'adblock-simple', error: `Invalid domain in adblock rule: ${domain}` };
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
429
|
+
// Removed: "Simple adblock format" branch for `||domain.com^` without
|
|
430
|
+
// modifiers. The main `||...^` branch above already handles this case
|
|
431
|
+
// (parts becomes ['domain.com', ''] on split, the empty modifier check
|
|
432
|
+
// falls through to the success return as format='adblock'). This branch
|
|
433
|
+
// was unreachable dead code.
|
|
434
|
+
|
|
384
435
|
// Localhost format: 127.0.0.1 domain.com or 0.0.0.0 domain.com
|
|
385
436
|
if (trimmedRule.match(/^(127\.0\.0\.1|0\.0\.0\.0)\s+/)) {
|
|
386
437
|
const parts = trimmedRule.split(/\s+/);
|
|
@@ -665,6 +716,44 @@ function validateSiteConfig(siteConfig, siteIndex = 0) {
|
|
|
665
716
|
}
|
|
666
717
|
});
|
|
667
718
|
|
|
719
|
+
// Cross-module validation: searchstring/searchstring_and. validateSearchString
|
|
720
|
+
// catches things like both-defined-at-once (forbidden), empty arrays, length
|
|
721
|
+
// caps, non-string elements. Before this call was added, misconfigured
|
|
722
|
+
// searchstring values silently passed validation and only surfaced as
|
|
723
|
+
// runtime TypeErrors mid-scan.
|
|
724
|
+
if (siteConfig.searchstring !== undefined || siteConfig.searchstring_and !== undefined) {
|
|
725
|
+
const ssValidation = validateSearchString(siteConfig.searchstring, siteConfig.searchstring_and);
|
|
726
|
+
if (!ssValidation.isValid) {
|
|
727
|
+
errors.push(`Site ${siteIndex}: ${ssValidation.error}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Cross-module validation: VPN configs. nwss.js dispatches on field
|
|
732
|
+
// presence — `vpn` → WireGuard, `openvpn` → OpenVPN. Both validators
|
|
733
|
+
// require a normalized config object, so normalize first. Previously
|
|
734
|
+
// VPN errors only surfaced inside connectForSite at scan time; now
|
|
735
|
+
// misconfigured configs fail loudly at startup.
|
|
736
|
+
if (siteConfig.vpn !== undefined && siteConfig.openvpn !== undefined) {
|
|
737
|
+
warnings.push(`Site ${siteIndex}: both 'vpn' (WireGuard) and 'openvpn' set — runtime dispatches to WireGuard, openvpn config will be ignored`);
|
|
738
|
+
}
|
|
739
|
+
if (siteConfig.vpn !== undefined) {
|
|
740
|
+
const normalized = normalizeWgConfig(siteConfig.vpn);
|
|
741
|
+
const vpnValidation = validateWgConfig(normalized);
|
|
742
|
+
if (!vpnValidation.isValid) {
|
|
743
|
+
vpnValidation.errors.forEach(e => errors.push(`Site ${siteIndex} (WireGuard): ${e}`));
|
|
744
|
+
}
|
|
745
|
+
// Validator warnings (e.g. "requires root") propagate too.
|
|
746
|
+
(vpnValidation.warnings || []).forEach(w => warnings.push(`Site ${siteIndex} (WireGuard): ${w}`));
|
|
747
|
+
}
|
|
748
|
+
if (siteConfig.openvpn !== undefined && siteConfig.vpn === undefined) {
|
|
749
|
+
const normalized = normalizeOvpnConfig(siteConfig.openvpn);
|
|
750
|
+
const ovpnValidation = validateOvpnConfig(normalized);
|
|
751
|
+
if (!ovpnValidation.isValid) {
|
|
752
|
+
ovpnValidation.errors.forEach(e => errors.push(`Site ${siteIndex} (OpenVPN): ${e}`));
|
|
753
|
+
}
|
|
754
|
+
(ovpnValidation.warnings || []).forEach(w => warnings.push(`Site ${siteIndex} (OpenVPN): ${w}`));
|
|
755
|
+
}
|
|
756
|
+
|
|
668
757
|
// Validate ignore_similar_threshold
|
|
669
758
|
if (siteConfig.ignore_similar_threshold !== undefined) {
|
|
670
759
|
if (typeof siteConfig.ignore_similar_threshold !== 'number' ||
|
|
@@ -774,7 +863,7 @@ function cleanRulesetFile(filePath, outputPath = null, options = {}) {
|
|
|
774
863
|
stats.duplicates++;
|
|
775
864
|
|
|
776
865
|
if (forceDebug) {
|
|
777
|
-
console.log(formatLogMessage('debug',
|
|
866
|
+
console.log(formatLogMessage('debug', `${CLEAN_TAG} Removing duplicate line ${lineNumber}: ${trimmed}`));
|
|
778
867
|
}
|
|
779
868
|
continue; // Skip duplicate
|
|
780
869
|
} else {
|
|
@@ -789,7 +878,7 @@ function cleanRulesetFile(filePath, outputPath = null, options = {}) {
|
|
|
789
878
|
stats.invalid++;
|
|
790
879
|
|
|
791
880
|
if (forceDebug) {
|
|
792
|
-
console.log(formatLogMessage('debug',
|
|
881
|
+
console.log(formatLogMessage('debug', `${CLEAN_TAG} Removing invalid line ${lineNumber}: ${trimmed} (${validation.error})`));
|
|
793
882
|
}
|
|
794
883
|
}
|
|
795
884
|
}
|
|
@@ -986,92 +1075,17 @@ function testDomainValidation() {
|
|
|
986
1075
|
return allPassed;
|
|
987
1076
|
}
|
|
988
1077
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
// Invalid rules
|
|
1002
|
-
{ rule: '', expected: false },
|
|
1003
|
-
{ rule: '||invalid..domain^', expected: false },
|
|
1004
|
-
{ rule: '||.example.com^', expected: false }
|
|
1005
|
-
];
|
|
1006
|
-
|
|
1007
|
-
let allPassed = true;
|
|
1008
|
-
|
|
1009
|
-
testCases.forEach(({ rule, expected }) => {
|
|
1010
|
-
const result = validateAdblockRule(rule);
|
|
1011
|
-
if (result.isValid !== expected) {
|
|
1012
|
-
console.error(`Test failed for rule "${rule}": expected ${expected}, got ${result.isValid}`);
|
|
1013
|
-
allPassed = false;
|
|
1014
|
-
}
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
return allPassed;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
/**
|
|
1021
|
-
* Validates a domain and formats it according to specified output options
|
|
1022
|
-
* @param {string} domain - The domain to validate and format
|
|
1023
|
-
* @param {object} options - Formatting options
|
|
1024
|
-
* @returns {string|object} Formatted domain string or error object
|
|
1025
|
-
*/
|
|
1026
|
-
function formatDomainWithValidation(domain, options = {}) {
|
|
1027
|
-
const {
|
|
1028
|
-
localhost = false,
|
|
1029
|
-
localhostAlt = false,
|
|
1030
|
-
plain = false,
|
|
1031
|
-
dnsmasq = false,
|
|
1032
|
-
dnsmasqOld = false,
|
|
1033
|
-
unbound = false,
|
|
1034
|
-
privoxy = false,
|
|
1035
|
-
pihole = false,
|
|
1036
|
-
adblockRules = false,
|
|
1037
|
-
resourceType = ''
|
|
1038
|
-
} = options;
|
|
1039
|
-
|
|
1040
|
-
// Validate domain first
|
|
1041
|
-
if (!isValidDomain(domain)) {
|
|
1042
|
-
return {
|
|
1043
|
-
isValid: false,
|
|
1044
|
-
error: `Invalid domain format: ${domain}`,
|
|
1045
|
-
formattedRule: null
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// Format according to specified options (priority order)
|
|
1050
|
-
if (pihole) {
|
|
1051
|
-
const escapedDomain = domain.replace(/\./g, '\\.');
|
|
1052
|
-
return `(^|\\.)${escapedDomain}$`;
|
|
1053
|
-
} else if (privoxy) {
|
|
1054
|
-
return `{ +block } .${domain}`;
|
|
1055
|
-
} else if (dnsmasq) {
|
|
1056
|
-
return `local=/${domain}/`;
|
|
1057
|
-
} else if (dnsmasqOld) {
|
|
1058
|
-
return `server=/${domain}/`;
|
|
1059
|
-
} else if (unbound) {
|
|
1060
|
-
return `local-zone: "${domain}." always_null`;
|
|
1061
|
-
} else if (localhost) {
|
|
1062
|
-
return `127.0.0.1 ${domain}`;
|
|
1063
|
-
} else if (localhostAlt) {
|
|
1064
|
-
return `0.0.0.0 ${domain}`;
|
|
1065
|
-
} else if (adblockRules && resourceType) {
|
|
1066
|
-
return `||${domain}^$${resourceType}`;
|
|
1067
|
-
} else if (plain) {
|
|
1068
|
-
return domain;
|
|
1069
|
-
} else {
|
|
1070
|
-
// Default adblock format
|
|
1071
|
-
return `||${domain}^`;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1078
|
+
// Public surface used by nwss.js (validateRulesetFile, validateFullConfig,
|
|
1079
|
+
// testDomainValidation, cleanRulesetFile). The rest (isValidDomain,
|
|
1080
|
+
// isValidDomainLabel, isValidTLD, isIPAddress, isIPv4, isIPv6,
|
|
1081
|
+
// validateRegexPattern, validateAdblockModifiers, validateAdblockRule,
|
|
1082
|
+
// validateSiteConfig) stay internal-helper-but-exported for now since
|
|
1083
|
+
// downstream callers MAY import them via the dotted path even if grep
|
|
1084
|
+
// shows no current consumers — domain validators are the kind of thing
|
|
1085
|
+
// that gets added to in future. testAdblockValidation and
|
|
1086
|
+
// formatDomainWithValidation were removed entirely (zero callers
|
|
1087
|
+
// anywhere; formatDomainWithValidation looked like an old implementation
|
|
1088
|
+
// superseded by lib/output.js).
|
|
1075
1089
|
module.exports = {
|
|
1076
1090
|
isValidDomain,
|
|
1077
1091
|
isValidDomainLabel,
|
|
@@ -1086,7 +1100,5 @@ module.exports = {
|
|
|
1086
1100
|
cleanRulesetFile,
|
|
1087
1101
|
validateSiteConfig,
|
|
1088
1102
|
validateFullConfig,
|
|
1089
|
-
testDomainValidation
|
|
1090
|
-
testAdblockValidation,
|
|
1091
|
-
formatDomainWithValidation
|
|
1103
|
+
testDomainValidation
|
|
1092
1104
|
};
|