@fanboynz/network-scanner 1.0.35
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 +33 -0
- package/JSONMANUAL.md +121 -0
- package/LICENSE +674 -0
- package/README.md +357 -0
- package/config.json +74 -0
- package/lib/browserexit.js +522 -0
- package/lib/browserhealth.js +308 -0
- package/lib/cloudflare.js +660 -0
- package/lib/colorize.js +168 -0
- package/lib/compare.js +159 -0
- package/lib/compress.js +129 -0
- package/lib/fingerprint.js +613 -0
- package/lib/flowproxy.js +274 -0
- package/lib/grep.js +348 -0
- package/lib/ignore_similar.js +237 -0
- package/lib/nettools.js +1200 -0
- package/lib/output.js +633 -0
- package/lib/redirect.js +384 -0
- package/lib/searchstring.js +561 -0
- package/lib/validate_rules.js +1107 -0
- package/nwss.1 +824 -0
- package/nwss.js +2488 -0
- package/package.json +45 -0
- package/regex-samples.md +27 -0
- package/scanner-script-org.js +588 -0
|
@@ -0,0 +1,1107 @@
|
|
|
1
|
+
const { formatLogMessage } = require('./colorize');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enhanced domain validation function
|
|
5
|
+
* @param {string} domain - The domain to validate
|
|
6
|
+
* @returns {boolean} True if domain is valid, false otherwise
|
|
7
|
+
*/
|
|
8
|
+
function isValidDomain(domain) {
|
|
9
|
+
if (!domain || typeof domain !== 'string') {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Trim whitespace
|
|
14
|
+
domain = domain.trim();
|
|
15
|
+
|
|
16
|
+
// Check minimum length (shortest valid domain is something like "a.b" = 3 chars)
|
|
17
|
+
if (domain.length < 3) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check maximum length (RFC 1035 - 253 characters max)
|
|
22
|
+
if (domain.length > 253) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for IP addresses (both IPv4 and IPv6)
|
|
27
|
+
if (isIPAddress(domain)) {
|
|
28
|
+
return true; // IP addresses are valid targets
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Must contain at least one dot
|
|
32
|
+
if (!domain.includes('.')) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Cannot start or end with dot
|
|
37
|
+
if (domain.startsWith('.') || domain.endsWith('.')) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cannot contain consecutive dots
|
|
42
|
+
if (domain.includes('..')) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Split into labels and validate each
|
|
47
|
+
const labels = domain.split('.');
|
|
48
|
+
|
|
49
|
+
// Must have at least 2 labels (domain.tld)
|
|
50
|
+
if (labels.length < 2) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validate each label
|
|
55
|
+
for (const label of labels) {
|
|
56
|
+
if (!isValidDomainLabel(label)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// TLD (last label) validation
|
|
62
|
+
const tld = labels[labels.length - 1];
|
|
63
|
+
if (!isValidTLD(tld)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates a single domain label
|
|
72
|
+
* @param {string} label - The label to validate
|
|
73
|
+
* @returns {boolean} True if label is valid
|
|
74
|
+
*/
|
|
75
|
+
function isValidDomainLabel(label) {
|
|
76
|
+
if (!label || label.length === 0) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Label cannot be longer than 63 characters (RFC 1035)
|
|
81
|
+
if (label.length > 63) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Label cannot start or end with hyphen
|
|
86
|
+
if (label.startsWith('-') || label.endsWith('-')) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Label can only contain alphanumeric characters and hyphens
|
|
91
|
+
const labelRegex = /^[a-zA-Z0-9-]+$/;
|
|
92
|
+
if (!labelRegex.test(label)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validates TLD (Top Level Domain)
|
|
101
|
+
* @param {string} tld - The TLD to validate
|
|
102
|
+
* @returns {boolean} True if TLD is valid
|
|
103
|
+
*/
|
|
104
|
+
function isValidTLD(tld) {
|
|
105
|
+
if (!tld || tld.length === 0) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// TLD must be at least 2 characters
|
|
110
|
+
if (tld.length < 2) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Allow numeric TLDs for modern domains like .1password
|
|
115
|
+
// but still validate structure
|
|
116
|
+
|
|
117
|
+
// TLD can contain letters and numbers, but must start with letter
|
|
118
|
+
const tldRegex = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
119
|
+
if (!tldRegex.test(tld)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks if a string is an IP address (IPv4 or IPv6)
|
|
128
|
+
* @param {string} str - String to check
|
|
129
|
+
* @returns {boolean} True if it's an IP address
|
|
130
|
+
*/
|
|
131
|
+
function isIPAddress(str) {
|
|
132
|
+
return isIPv4(str) || isIPv6(str);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Checks if a string is a valid IPv4 address
|
|
137
|
+
* @param {string} str - String to check
|
|
138
|
+
* @returns {boolean} True if valid IPv4
|
|
139
|
+
*/
|
|
140
|
+
function isIPv4(str) {
|
|
141
|
+
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
142
|
+
return ipv4Regex.test(str);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks if a string is a valid IPv6 address
|
|
147
|
+
* @param {string} str - String to check
|
|
148
|
+
* @returns {boolean} True if valid IPv6
|
|
149
|
+
*/
|
|
150
|
+
function isIPv6(str) {
|
|
151
|
+
// Simplified IPv6 regex - covers most common cases
|
|
152
|
+
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::$|^::1$|^(?:[0-9a-fA-F]{1,4}:)*::(?:[0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4}$/;
|
|
153
|
+
return ipv6Regex.test(str);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validates a regex pattern string
|
|
158
|
+
* @param {string} pattern - The regex pattern to validate
|
|
159
|
+
* @returns {object} Validation result with isValid boolean and error message
|
|
160
|
+
*/
|
|
161
|
+
function validateRegexPattern(pattern) {
|
|
162
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
163
|
+
return { isValid: false, error: 'Pattern must be a non-empty string' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Remove leading/trailing slashes if present
|
|
168
|
+
const cleanPattern = pattern.replace(/^\/(.*)\/$/, '$1');
|
|
169
|
+
new RegExp(cleanPattern);
|
|
170
|
+
return { isValid: true };
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return { isValid: false, error: `Invalid regex: ${err.message}` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Validates adblock filter modifiers
|
|
178
|
+
* @param {string} modifiers - The modifier string (e.g., "script,third-party")
|
|
179
|
+
* @returns {object} Validation result
|
|
180
|
+
*/
|
|
181
|
+
function validateAdblockModifiers(modifiers) {
|
|
182
|
+
if (!modifiers) {
|
|
183
|
+
return { isValid: true, modifiers: [] };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Valid adblock filter modifiers
|
|
187
|
+
const validModifiers = new Set([
|
|
188
|
+
// Resource type modifiers
|
|
189
|
+
'script', 'stylesheet', 'image', 'object', 'xmlhttprequest', 'subdocument',
|
|
190
|
+
'ping', 'websocket', 'webrtc', 'document', 'elemhide', 'generichide',
|
|
191
|
+
'genericblock', 'popup', 'font', 'media', 'other',
|
|
192
|
+
|
|
193
|
+
// Party modifiers
|
|
194
|
+
'third-party', 'first-party', '~third-party', '~first-party',
|
|
195
|
+
|
|
196
|
+
// Domain modifiers (domain= will be validated separately)
|
|
197
|
+
'domain',
|
|
198
|
+
|
|
199
|
+
// Method modifiers
|
|
200
|
+
'match-case', '~match-case',
|
|
201
|
+
|
|
202
|
+
// Action modifiers
|
|
203
|
+
'important', 'badfilter',
|
|
204
|
+
|
|
205
|
+
// CSP and redirect modifiers
|
|
206
|
+
'csp', 'redirect', 'redirect-rule',
|
|
207
|
+
|
|
208
|
+
// uBlock Origin specific
|
|
209
|
+
'inline-script', 'inline-font', 'mp4', 'empty', 'xhr'
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const modifierList = modifiers.split(',').map(m => m.trim());
|
|
213
|
+
const invalidModifiers = [];
|
|
214
|
+
const parsedModifiers = [];
|
|
215
|
+
|
|
216
|
+
for (const modifier of modifierList) {
|
|
217
|
+
if (!modifier) continue;
|
|
218
|
+
|
|
219
|
+
// Handle domain= modifier specially
|
|
220
|
+
if (modifier.startsWith('domain=')) {
|
|
221
|
+
const domains = modifier.substring(7);
|
|
222
|
+
if (domains) {
|
|
223
|
+
// Validate domain list format (domains separated by |)
|
|
224
|
+
const domainList = domains.split('|');
|
|
225
|
+
for (const domain of domainList) {
|
|
226
|
+
const cleanDomain = domain.startsWith('~') ? domain.substring(1) : domain;
|
|
227
|
+
if (cleanDomain && !isValidDomain(cleanDomain)) {
|
|
228
|
+
invalidModifiers.push(`Invalid domain in domain= modifier: ${cleanDomain}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
parsedModifiers.push({ type: 'domain', value: domains });
|
|
232
|
+
} else {
|
|
233
|
+
invalidModifiers.push('Empty domain= modifier');
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle csp= modifier
|
|
239
|
+
if (modifier.startsWith('csp=')) {
|
|
240
|
+
const cspValue = modifier.substring(4);
|
|
241
|
+
if (!cspValue) {
|
|
242
|
+
invalidModifiers.push('Empty csp= modifier');
|
|
243
|
+
} else {
|
|
244
|
+
parsedModifiers.push({ type: 'csp', value: cspValue });
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle redirect= modifier
|
|
250
|
+
if (modifier.startsWith('redirect=')) {
|
|
251
|
+
const redirectValue = modifier.substring(9);
|
|
252
|
+
if (!redirectValue) {
|
|
253
|
+
invalidModifiers.push('Empty redirect= modifier');
|
|
254
|
+
} else {
|
|
255
|
+
parsedModifiers.push({ type: 'redirect', value: redirectValue });
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check for negated modifiers (starting with ~)
|
|
261
|
+
const isNegated = modifier.startsWith('~');
|
|
262
|
+
const baseModifier = isNegated ? modifier.substring(1) : modifier;
|
|
263
|
+
|
|
264
|
+
if (validModifiers.has(modifier) || validModifiers.has(baseModifier)) {
|
|
265
|
+
parsedModifiers.push({
|
|
266
|
+
type: baseModifier,
|
|
267
|
+
negated: isNegated,
|
|
268
|
+
raw: modifier
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
invalidModifiers.push(modifier);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (invalidModifiers.length > 0) {
|
|
276
|
+
return {
|
|
277
|
+
isValid: false,
|
|
278
|
+
error: `Invalid modifiers: ${invalidModifiers.join(', ')}`,
|
|
279
|
+
validModifiers: parsedModifiers
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
isValid: true,
|
|
285
|
+
modifiers: parsedModifiers
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Validates adblock rule format with comprehensive modifier support
|
|
291
|
+
* @param {string} rule - The rule to validate
|
|
292
|
+
* @returns {object} Validation result with format type and validity
|
|
293
|
+
*/
|
|
294
|
+
function validateAdblockRule(rule) {
|
|
295
|
+
if (!rule || typeof rule !== 'string') {
|
|
296
|
+
return { isValid: false, format: 'unknown', error: 'Rule must be a non-empty string' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const trimmedRule = rule.trim();
|
|
300
|
+
|
|
301
|
+
// Skip comments
|
|
302
|
+
if (trimmedRule.startsWith('!') || trimmedRule.startsWith('#')) {
|
|
303
|
+
return { isValid: true, format: 'comment' };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Adblock format: ||domain.com^ or ||domain.com^$script,third-party
|
|
307
|
+
if (trimmedRule.startsWith('||') && trimmedRule.includes('^')) {
|
|
308
|
+
const parts = trimmedRule.substring(2).split('^');
|
|
309
|
+
const domain = parts[0];
|
|
310
|
+
|
|
311
|
+
if (!isValidDomain(domain)) {
|
|
312
|
+
return { isValid: false, format: 'adblock', error: `Invalid domain in adblock rule: ${domain}` };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for modifiers after ^$
|
|
316
|
+
let modifiers = '';
|
|
317
|
+
let modifierValidation = { isValid: true, modifiers: [] };
|
|
318
|
+
|
|
319
|
+
if (parts.length > 1 && parts[1].startsWith('$')) {
|
|
320
|
+
modifiers = parts[1].substring(1);
|
|
321
|
+
modifierValidation = validateAdblockModifiers(modifiers);
|
|
322
|
+
|
|
323
|
+
if (!modifierValidation.isValid) {
|
|
324
|
+
return {
|
|
325
|
+
isValid: false,
|
|
326
|
+
format: 'adblock',
|
|
327
|
+
error: `${modifierValidation.error} in rule: ${trimmedRule}`,
|
|
328
|
+
domain,
|
|
329
|
+
modifiers: modifierValidation.validModifiers || []
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
isValid: true,
|
|
336
|
+
format: 'adblock',
|
|
337
|
+
domain,
|
|
338
|
+
modifiers: modifierValidation.modifiers,
|
|
339
|
+
hasModifiers: modifiers.length > 0
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Basic adblock format without ||: domain.com^$modifier
|
|
344
|
+
if (trimmedRule.includes('^') && trimmedRule.includes('$')) {
|
|
345
|
+
const parts = trimmedRule.split('^$');
|
|
346
|
+
if (parts.length === 2) {
|
|
347
|
+
const domain = parts[0];
|
|
348
|
+
const modifiers = parts[1];
|
|
349
|
+
|
|
350
|
+
if (!isValidDomain(domain)) {
|
|
351
|
+
return { isValid: false, format: 'adblock-basic', error: `Invalid domain in adblock rule: ${domain}` };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const modifierValidation = validateAdblockModifiers(modifiers);
|
|
355
|
+
if (!modifierValidation.isValid) {
|
|
356
|
+
return {
|
|
357
|
+
isValid: false,
|
|
358
|
+
format: 'adblock-basic',
|
|
359
|
+
error: modifierValidation.error,
|
|
360
|
+
domain
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
isValid: true,
|
|
366
|
+
format: 'adblock-basic',
|
|
367
|
+
domain,
|
|
368
|
+
modifiers: modifierValidation.modifiers
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Simple adblock format: ||domain.com^ (without modifiers)
|
|
374
|
+
if (trimmedRule.startsWith('||') && trimmedRule.endsWith('^')) {
|
|
375
|
+
const domain = trimmedRule.substring(2, trimmedRule.length - 1);
|
|
376
|
+
if (isValidDomain(domain)) {
|
|
377
|
+
return { isValid: true, format: 'adblock-simple', domain };
|
|
378
|
+
} else {
|
|
379
|
+
return { isValid: false, format: 'adblock-simple', error: `Invalid domain in adblock rule: ${domain}` };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Localhost format: 127.0.0.1 domain.com or 0.0.0.0 domain.com
|
|
384
|
+
if (trimmedRule.match(/^(127\.0\.0\.1|0\.0\.0\.0)\s+/)) {
|
|
385
|
+
const parts = trimmedRule.split(/\s+/);
|
|
386
|
+
if (parts.length >= 2) {
|
|
387
|
+
const domain = parts[1];
|
|
388
|
+
if (isValidDomain(domain)) {
|
|
389
|
+
return { isValid: true, format: 'localhost', domain };
|
|
390
|
+
} else {
|
|
391
|
+
return { isValid: false, format: 'localhost', error: `Invalid domain in localhost rule: ${domain}` };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { isValid: false, format: 'localhost', error: 'Malformed localhost rule' };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// DNSmasq format: local=/domain.com/
|
|
398
|
+
if (trimmedRule.startsWith('local=/') && trimmedRule.endsWith('/')) {
|
|
399
|
+
const domain = trimmedRule.substring(6, trimmedRule.length - 1);
|
|
400
|
+
if (isValidDomain(domain)) {
|
|
401
|
+
return { isValid: true, format: 'dnsmasq', domain };
|
|
402
|
+
} else {
|
|
403
|
+
return { isValid: false, format: 'dnsmasq', error: `Invalid domain in dnsmasq rule: ${domain}` };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// DNSmasq old format: server=/domain.com/
|
|
408
|
+
if (trimmedRule.startsWith('server=/') && trimmedRule.endsWith('/')) {
|
|
409
|
+
const domain = trimmedRule.substring(7, trimmedRule.length - 1);
|
|
410
|
+
if (isValidDomain(domain)) {
|
|
411
|
+
return { isValid: true, format: 'dnsmasq-old', domain };
|
|
412
|
+
} else {
|
|
413
|
+
return { isValid: false, format: 'dnsmasq-old', error: `Invalid domain in dnsmasq-old rule: ${domain}` };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Unbound format: local-zone: "domain.com." always_null
|
|
418
|
+
if (trimmedRule.startsWith('local-zone: "') && trimmedRule.includes('" always_null')) {
|
|
419
|
+
const domain = trimmedRule.substring(13).split('"')[0];
|
|
420
|
+
const cleanDomain = domain.endsWith('.') ? domain.slice(0, -1) : domain;
|
|
421
|
+
if (isValidDomain(cleanDomain)) {
|
|
422
|
+
return { isValid: true, format: 'unbound', domain: cleanDomain };
|
|
423
|
+
} else {
|
|
424
|
+
return { isValid: false, format: 'unbound', error: `Invalid domain in unbound rule: ${cleanDomain}` };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Privoxy format: { +block } .domain.com
|
|
429
|
+
if (trimmedRule.startsWith('{ +block } .')) {
|
|
430
|
+
const domain = trimmedRule.substring(12);
|
|
431
|
+
if (isValidDomain(domain)) {
|
|
432
|
+
return { isValid: true, format: 'privoxy', domain };
|
|
433
|
+
} else {
|
|
434
|
+
return { isValid: false, format: 'privoxy', error: `Invalid domain in privoxy rule: ${domain}` };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Pi-hole regex format: (^|\.)domain\.com$
|
|
439
|
+
if (trimmedRule.match(/^\(\^\|\\?\.\).*\$$/)) {
|
|
440
|
+
const domain = trimmedRule.replace(/^\(\^\|\\?\.\)/, '').replace(/\\\./g, '.').replace(/\$$/, '');
|
|
441
|
+
if (isValidDomain(domain)) {
|
|
442
|
+
return { isValid: true, format: 'pihole', domain };
|
|
443
|
+
} else {
|
|
444
|
+
return { isValid: false, format: 'pihole', error: `Invalid domain in pihole rule: ${domain}` };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Plain domain format
|
|
449
|
+
if (isValidDomain(trimmedRule)) {
|
|
450
|
+
return { isValid: true, format: 'plain', domain: trimmedRule };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return { isValid: false, format: 'unknown', error: 'Unrecognized rule format' };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Validates an entire ruleset file
|
|
458
|
+
* @param {string} filePath - Path to the file to validate
|
|
459
|
+
* @param {object} options - Validation options
|
|
460
|
+
* @returns {object} Validation results with statistics and errors
|
|
461
|
+
*/
|
|
462
|
+
function validateRulesetFile(filePath, options = {}) {
|
|
463
|
+
const {
|
|
464
|
+
forceDebug = false,
|
|
465
|
+
silentMode = false,
|
|
466
|
+
maxErrors = 10
|
|
467
|
+
} = options;
|
|
468
|
+
|
|
469
|
+
const fs = require('fs');
|
|
470
|
+
|
|
471
|
+
if (!fs.existsSync(filePath)) {
|
|
472
|
+
return {
|
|
473
|
+
isValid: false,
|
|
474
|
+
error: `File not found: ${filePath}`,
|
|
475
|
+
stats: { total: 0, valid: 0, invalid: 0, comments: 0 }
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
let content;
|
|
480
|
+
try {
|
|
481
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
482
|
+
} catch (err) {
|
|
483
|
+
return {
|
|
484
|
+
isValid: false,
|
|
485
|
+
error: `Failed to read file: ${err.message}`,
|
|
486
|
+
stats: { total: 0, valid: 0, invalid: 0, comments: 0 }
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const lines = content.split('\n');
|
|
491
|
+
const stats = {
|
|
492
|
+
total: 0,
|
|
493
|
+
valid: 0,
|
|
494
|
+
invalid: 0,
|
|
495
|
+
comments: 0,
|
|
496
|
+
formats: {}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const errors = [];
|
|
500
|
+
const duplicates = new Set();
|
|
501
|
+
const seenRules = new Set();
|
|
502
|
+
|
|
503
|
+
for (let i = 0; i < lines.length; i++) {
|
|
504
|
+
const line = lines[i].trim();
|
|
505
|
+
|
|
506
|
+
// Skip empty lines
|
|
507
|
+
if (!line) continue;
|
|
508
|
+
|
|
509
|
+
stats.total++;
|
|
510
|
+
const lineNumber = i + 1;
|
|
511
|
+
|
|
512
|
+
const validation = validateAdblockRule(line);
|
|
513
|
+
|
|
514
|
+
if (validation.format === 'comment') {
|
|
515
|
+
stats.comments++;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (validation.isValid) {
|
|
520
|
+
stats.valid++;
|
|
521
|
+
|
|
522
|
+
// Track format types
|
|
523
|
+
if (!stats.formats[validation.format]) {
|
|
524
|
+
stats.formats[validation.format] = 0;
|
|
525
|
+
}
|
|
526
|
+
stats.formats[validation.format]++;
|
|
527
|
+
|
|
528
|
+
// Check for duplicates
|
|
529
|
+
if (seenRules.has(line)) {
|
|
530
|
+
duplicates.add(line);
|
|
531
|
+
if (forceDebug) {
|
|
532
|
+
errors.push(`Line ${lineNumber}: Duplicate rule - ${line}`);
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
seenRules.add(line);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
stats.invalid++;
|
|
539
|
+
errors.push(`Line ${lineNumber}: ${validation.error} - ${line}`);
|
|
540
|
+
|
|
541
|
+
if (errors.length >= maxErrors) {
|
|
542
|
+
errors.push(`... (stopping after ${maxErrors} errors, ${stats.total - i - 1} lines remaining)`);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Log validation results
|
|
549
|
+
if (!silentMode) {
|
|
550
|
+
if (forceDebug) {
|
|
551
|
+
console.log(formatLogMessage('debug', `Validated ${filePath}:`));
|
|
552
|
+
console.log(formatLogMessage('debug', ` Total lines: ${stats.total} (${stats.comments} comments)`));
|
|
553
|
+
console.log(formatLogMessage('debug', ` Valid rules: ${stats.valid}`));
|
|
554
|
+
console.log(formatLogMessage('debug', ` Invalid rules: ${stats.invalid}`));
|
|
555
|
+
console.log(formatLogMessage('debug', ` Duplicates found: ${duplicates.size}`));
|
|
556
|
+
|
|
557
|
+
if (Object.keys(stats.formats).length > 0) {
|
|
558
|
+
console.log(formatLogMessage('debug', ` Format breakdown:`));
|
|
559
|
+
Object.entries(stats.formats).forEach(([format, count]) => {
|
|
560
|
+
console.log(formatLogMessage('debug', ` ${format}: ${count}`));
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (errors.length > 0) {
|
|
566
|
+
console.log(formatLogMessage('warn', `Validation errors in ${filePath}:`));
|
|
567
|
+
errors.slice(0, 5).forEach(error => {
|
|
568
|
+
console.log(formatLogMessage('warn', ` ${error}`));
|
|
569
|
+
});
|
|
570
|
+
if (errors.length > 5) {
|
|
571
|
+
console.log(formatLogMessage('warn', ` ... and ${errors.length - 5} more errors`));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
isValid: stats.invalid === 0,
|
|
578
|
+
stats,
|
|
579
|
+
errors,
|
|
580
|
+
duplicates: Array.from(duplicates),
|
|
581
|
+
filePath
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Validates configuration object for site settings
|
|
587
|
+
* @param {object} siteConfig - Site configuration to validate
|
|
588
|
+
* @param {number} siteIndex - Index of the site for error reporting
|
|
589
|
+
* @returns {object} Validation result with warnings and errors
|
|
590
|
+
*/
|
|
591
|
+
function validateSiteConfig(siteConfig, siteIndex = 0) {
|
|
592
|
+
const warnings = [];
|
|
593
|
+
const errors = [];
|
|
594
|
+
|
|
595
|
+
// Check required fields
|
|
596
|
+
if (!siteConfig.url) {
|
|
597
|
+
errors.push(`Site ${siteIndex}: Missing required 'url' field`);
|
|
598
|
+
} else {
|
|
599
|
+
// Validate URLs
|
|
600
|
+
const urls = Array.isArray(siteConfig.url) ? siteConfig.url : [siteConfig.url];
|
|
601
|
+
urls.forEach((url, urlIndex) => {
|
|
602
|
+
try {
|
|
603
|
+
new URL(url);
|
|
604
|
+
} catch (urlErr) {
|
|
605
|
+
errors.push(`Site ${siteIndex}, URL ${urlIndex}: Invalid URL format - ${url}`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Validate regex patterns
|
|
611
|
+
if (siteConfig.filterRegex) {
|
|
612
|
+
const regexes = Array.isArray(siteConfig.filterRegex) ? siteConfig.filterRegex : [siteConfig.filterRegex];
|
|
613
|
+
regexes.forEach((pattern, patternIndex) => {
|
|
614
|
+
const validation = validateRegexPattern(pattern);
|
|
615
|
+
if (!validation.isValid) {
|
|
616
|
+
errors.push(`Site ${siteIndex}, filterRegex ${patternIndex}: ${validation.error}`);
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Validate blocked patterns
|
|
622
|
+
if (siteConfig.blocked) {
|
|
623
|
+
if (!Array.isArray(siteConfig.blocked)) {
|
|
624
|
+
errors.push(`Site ${siteIndex}: 'blocked' must be an array`);
|
|
625
|
+
} else {
|
|
626
|
+
siteConfig.blocked.forEach((pattern, patternIndex) => {
|
|
627
|
+
const validation = validateRegexPattern(pattern);
|
|
628
|
+
if (!validation.isValid) {
|
|
629
|
+
errors.push(`Site ${siteIndex}, blocked ${patternIndex}: ${validation.error}`);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Validate resource types
|
|
636
|
+
if (siteConfig.resourceTypes) {
|
|
637
|
+
if (!Array.isArray(siteConfig.resourceTypes)) {
|
|
638
|
+
errors.push(`Site ${siteIndex}: 'resourceTypes' must be an array`);
|
|
639
|
+
} else {
|
|
640
|
+
const validTypes = ['script', 'stylesheet', 'image', 'font', 'document', 'subdocument', 'xhr', 'fetch', 'websocket', 'media', 'ping', 'other'];
|
|
641
|
+
siteConfig.resourceTypes.forEach(type => {
|
|
642
|
+
if (!validTypes.includes(type)) {
|
|
643
|
+
warnings.push(`Site ${siteIndex}: Unknown resourceType '${type}'. Valid types: ${validTypes.join(', ')}`);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Validate CSS selectors
|
|
650
|
+
if (siteConfig.css_blocked) {
|
|
651
|
+
if (!Array.isArray(siteConfig.css_blocked)) {
|
|
652
|
+
errors.push(`Site ${siteIndex}: 'css_blocked' must be an array`);
|
|
653
|
+
}
|
|
654
|
+
// Note: CSS selector validation would be complex, skipping for now
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Validate numeric fields
|
|
658
|
+
const numericFields = ['delay', 'reload', 'timeout'];
|
|
659
|
+
numericFields.forEach(field => {
|
|
660
|
+
if (siteConfig[field] !== undefined) {
|
|
661
|
+
if (typeof siteConfig[field] !== 'number' || siteConfig[field] < 0) {
|
|
662
|
+
errors.push(`Site ${siteIndex}: '${field}' must be a positive number`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Validate boolean fields
|
|
668
|
+
const booleanFields = ['interact', 'clear_sitedata', 'firstParty', 'thirdParty', 'screenshot', 'headful', 'ignore_similar', 'ignore_similar_ignored_domains'];
|
|
669
|
+
booleanFields.forEach(field => {
|
|
670
|
+
if (siteConfig[field] !== undefined && typeof siteConfig[field] !== 'boolean') {
|
|
671
|
+
warnings.push(`Site ${siteIndex}: '${field}' should be a boolean (true/false)`);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Validate ignore_similar_threshold
|
|
676
|
+
if (siteConfig.ignore_similar_threshold !== undefined) {
|
|
677
|
+
if (typeof siteConfig.ignore_similar_threshold !== 'number' ||
|
|
678
|
+
siteConfig.ignore_similar_threshold < 0 ||
|
|
679
|
+
siteConfig.ignore_similar_threshold > 100) {
|
|
680
|
+
errors.push(`Site ${siteIndex}: 'ignore_similar_threshold' must be a number between 0 and 100`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Validate user agent
|
|
685
|
+
if (siteConfig.userAgent) {
|
|
686
|
+
const validUserAgents = ['chrome', 'firefox', 'safari'];
|
|
687
|
+
if (!validUserAgents.includes(siteConfig.userAgent.toLowerCase())) {
|
|
688
|
+
warnings.push(`Site ${siteIndex}: Unknown userAgent '${siteConfig.userAgent}'. Valid options: ${validUserAgents.join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Check for conflicting output format options
|
|
693
|
+
const outputFormats = ['localhost', 'localhost_0_0_0_0', 'plain', 'dnsmasq', 'dnsmasq_old', 'unbound', 'privoxy', 'pihole', 'adblock_rules'];
|
|
694
|
+
const enabledFormats = outputFormats.filter(format => siteConfig[format] === true);
|
|
695
|
+
if (enabledFormats.length > 1) {
|
|
696
|
+
warnings.push(`Site ${siteIndex}: Multiple output formats enabled (${enabledFormats.join(', ')}). Only one should be used.`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
isValid: errors.length === 0,
|
|
701
|
+
warnings,
|
|
702
|
+
errors
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Cleans a ruleset file by removing invalid lines and optionally duplicates
|
|
708
|
+
* @param {string} filePath - Path to the file to clean
|
|
709
|
+
* @param {string} outputPath - Optional output path (defaults to overwriting input file)
|
|
710
|
+
* @param {object} options - Cleaning options
|
|
711
|
+
* @returns {object} Cleaning results with statistics
|
|
712
|
+
*/
|
|
713
|
+
function cleanRulesetFile(filePath, outputPath = null, options = {}) {
|
|
714
|
+
const {
|
|
715
|
+
forceDebug = false,
|
|
716
|
+
silentMode = false,
|
|
717
|
+
removeDuplicates = false,
|
|
718
|
+
backupOriginal = true,
|
|
719
|
+
dryRun = false
|
|
720
|
+
} = options;
|
|
721
|
+
|
|
722
|
+
const fs = require('fs');
|
|
723
|
+
const path = require('path');
|
|
724
|
+
|
|
725
|
+
if (!fs.existsSync(filePath)) {
|
|
726
|
+
return {
|
|
727
|
+
success: false,
|
|
728
|
+
error: `File not found: ${filePath}`,
|
|
729
|
+
stats: { total: 0, valid: 0, invalid: 0, removed: 0, duplicates: 0 }
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
let content;
|
|
734
|
+
try {
|
|
735
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
736
|
+
} catch (err) {
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
error: `Failed to read file: ${err.message}`,
|
|
740
|
+
stats: { total: 0, valid: 0, invalid: 0, removed: 0, duplicates: 0 }
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const lines = content.split('\n');
|
|
745
|
+
const validLines = [];
|
|
746
|
+
const invalidLines = [];
|
|
747
|
+
const seenRules = new Set();
|
|
748
|
+
const duplicateLines = [];
|
|
749
|
+
|
|
750
|
+
const stats = {
|
|
751
|
+
total: 0,
|
|
752
|
+
valid: 0,
|
|
753
|
+
invalid: 0,
|
|
754
|
+
removed: 0,
|
|
755
|
+
duplicates: 0,
|
|
756
|
+
comments: 0,
|
|
757
|
+
empty: 0
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
for (let i = 0; i < lines.length; i++) {
|
|
761
|
+
const line = lines[i];
|
|
762
|
+
const trimmed = line.trim();
|
|
763
|
+
|
|
764
|
+
// Keep empty lines for formatting
|
|
765
|
+
if (!trimmed) {
|
|
766
|
+
validLines.push(line);
|
|
767
|
+
stats.empty++;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
stats.total++;
|
|
772
|
+
const lineNumber = i + 1;
|
|
773
|
+
|
|
774
|
+
const validation = validateAdblockRule(trimmed);
|
|
775
|
+
|
|
776
|
+
// Comments are always valid
|
|
777
|
+
if (validation.format === 'comment') {
|
|
778
|
+
validLines.push(line);
|
|
779
|
+
stats.valid++;
|
|
780
|
+
stats.comments++;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (validation.isValid) {
|
|
785
|
+
// Check for duplicates if requested
|
|
786
|
+
if (removeDuplicates) {
|
|
787
|
+
if (seenRules.has(trimmed)) {
|
|
788
|
+
duplicateLines.push({ line: trimmed, lineNumber });
|
|
789
|
+
stats.duplicates++;
|
|
790
|
+
|
|
791
|
+
if (forceDebug) {
|
|
792
|
+
console.log(formatLogMessage('debug', `[clean] Removing duplicate line ${lineNumber}: ${trimmed}`));
|
|
793
|
+
}
|
|
794
|
+
continue; // Skip duplicate
|
|
795
|
+
} else {
|
|
796
|
+
seenRules.add(trimmed);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
validLines.push(line);
|
|
801
|
+
stats.valid++;
|
|
802
|
+
} else {
|
|
803
|
+
invalidLines.push({ line: trimmed, lineNumber, error: validation.error });
|
|
804
|
+
stats.invalid++;
|
|
805
|
+
|
|
806
|
+
if (forceDebug) {
|
|
807
|
+
console.log(formatLogMessage('debug', `[clean] Removing invalid line ${lineNumber}: ${trimmed} (${validation.error})`));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
stats.removed = stats.invalid + stats.duplicates;
|
|
813
|
+
|
|
814
|
+
// Log cleaning results
|
|
815
|
+
if (!silentMode) {
|
|
816
|
+
if (forceDebug) {
|
|
817
|
+
console.log(formatLogMessage('debug', `Cleaning results for ${filePath}:`));
|
|
818
|
+
console.log(formatLogMessage('debug', ` Total lines processed: ${stats.total}`));
|
|
819
|
+
console.log(formatLogMessage('debug', ` Valid rules: ${stats.valid} (${stats.comments} comments)`));
|
|
820
|
+
console.log(formatLogMessage('debug', ` Invalid rules: ${stats.invalid}`));
|
|
821
|
+
console.log(formatLogMessage('debug', ` Duplicates: ${stats.duplicates}`));
|
|
822
|
+
console.log(formatLogMessage('debug', ` Total removed: ${stats.removed}`));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (invalidLines.length > 0 && forceDebug) {
|
|
826
|
+
console.log(formatLogMessage('warn', `Invalid lines found:`));
|
|
827
|
+
invalidLines.slice(0, 5).forEach(item => {
|
|
828
|
+
console.log(formatLogMessage('warn', ` Line ${item.lineNumber}: ${item.error}`));
|
|
829
|
+
});
|
|
830
|
+
if (invalidLines.length > 5) {
|
|
831
|
+
console.log(formatLogMessage('warn', ` ... and ${invalidLines.length - 5} more invalid lines`));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Create cleaned content
|
|
837
|
+
const cleanedContent = validLines.join('\n');
|
|
838
|
+
|
|
839
|
+
// Determine output path
|
|
840
|
+
const finalOutputPath = outputPath || filePath;
|
|
841
|
+
|
|
842
|
+
// Create backup if requested and not in dry run mode
|
|
843
|
+
if (backupOriginal && !dryRun && finalOutputPath === filePath) {
|
|
844
|
+
try {
|
|
845
|
+
const backupPath = `${filePath}.backup`;
|
|
846
|
+
fs.copyFileSync(filePath, backupPath);
|
|
847
|
+
if (forceDebug) {
|
|
848
|
+
console.log(formatLogMessage('debug', `Created backup: ${backupPath}`));
|
|
849
|
+
}
|
|
850
|
+
} catch (backupErr) {
|
|
851
|
+
return {
|
|
852
|
+
success: false,
|
|
853
|
+
error: `Failed to create backup: ${backupErr.message}`,
|
|
854
|
+
stats
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Write cleaned file (unless dry run)
|
|
860
|
+
if (!dryRun) {
|
|
861
|
+
try {
|
|
862
|
+
fs.writeFileSync(finalOutputPath, cleanedContent);
|
|
863
|
+
if (forceDebug) {
|
|
864
|
+
console.log(formatLogMessage('debug', `Wrote cleaned file: ${finalOutputPath}`));
|
|
865
|
+
}
|
|
866
|
+
} catch (writeErr) {
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
error: `Failed to write cleaned file: ${writeErr.message}`,
|
|
870
|
+
stats
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
success: true,
|
|
877
|
+
stats,
|
|
878
|
+
invalidLines,
|
|
879
|
+
duplicateLines,
|
|
880
|
+
modified: stats.removed > 0,
|
|
881
|
+
wouldModify: dryRun && stats.removed > 0,
|
|
882
|
+
backupCreated: backupOriginal && !dryRun && finalOutputPath === filePath
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Validates full configuration object
|
|
888
|
+
* @param {object} config - Complete configuration object
|
|
889
|
+
* @param {object} options - Validation options
|
|
890
|
+
* @returns {object} Comprehensive validation result
|
|
891
|
+
*/
|
|
892
|
+
function validateFullConfig(config, options = {}) {
|
|
893
|
+
const { forceDebug = false, silentMode = false } = options;
|
|
894
|
+
const globalErrors = [];
|
|
895
|
+
const siteValidations = [];
|
|
896
|
+
|
|
897
|
+
// Validate global configuration
|
|
898
|
+
if (!config) {
|
|
899
|
+
return {
|
|
900
|
+
isValid: false,
|
|
901
|
+
globalErrors: ['Configuration object is required'],
|
|
902
|
+
siteValidations: [],
|
|
903
|
+
summary: { totalSites: 0, validSites: 0, sitesWithErrors: 0, sitesWithWarnings: 0 }
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Validate sites array
|
|
908
|
+
if (!config.sites || !Array.isArray(config.sites)) {
|
|
909
|
+
globalErrors.push('Configuration must contain a "sites" array');
|
|
910
|
+
} else if (config.sites.length === 0) {
|
|
911
|
+
globalErrors.push('Sites array cannot be empty');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Validate global blocked patterns
|
|
915
|
+
if (config.blocked && !Array.isArray(config.blocked)) {
|
|
916
|
+
globalErrors.push('Global "blocked" must be an array');
|
|
917
|
+
} else if (config.blocked) {
|
|
918
|
+
config.blocked.forEach((pattern, index) => {
|
|
919
|
+
const validation = validateRegexPattern(pattern);
|
|
920
|
+
if (!validation.isValid) {
|
|
921
|
+
globalErrors.push(`Global blocked pattern ${index}: ${validation.error}`);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Validate global ignore_similar settings
|
|
927
|
+
if (config.ignore_similar !== undefined && typeof config.ignore_similar !== 'boolean') {
|
|
928
|
+
globalErrors.push('Global "ignore_similar" must be a boolean (true/false)');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (config.ignore_similar_threshold !== undefined) {
|
|
932
|
+
if (typeof config.ignore_similar_threshold !== 'number' ||
|
|
933
|
+
config.ignore_similar_threshold < 0 ||
|
|
934
|
+
config.ignore_similar_threshold > 100) {
|
|
935
|
+
globalErrors.push('Global "ignore_similar_threshold" must be a number between 0 and 100');
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (config.ignore_similar_ignored_domains !== undefined && typeof config.ignore_similar_ignored_domains !== 'boolean') {
|
|
940
|
+
globalErrors.push('Global "ignore_similar_ignored_domains" must be a boolean (true/false)');
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Validate individual sites
|
|
944
|
+
if (config.sites && Array.isArray(config.sites)) {
|
|
945
|
+
config.sites.forEach((site, index) => {
|
|
946
|
+
const siteValidation = validateSiteConfig(site, index);
|
|
947
|
+
siteValidations.push(siteValidation);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Calculate summary
|
|
952
|
+
const summary = {
|
|
953
|
+
totalSites: siteValidations.length,
|
|
954
|
+
validSites: siteValidations.filter(v => v.isValid).length,
|
|
955
|
+
sitesWithErrors: siteValidations.filter(v => v.errors.length > 0).length,
|
|
956
|
+
sitesWithWarnings: siteValidations.filter(v => v.warnings.length > 0).length
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
const isValid = globalErrors.length === 0 && summary.sitesWithErrors === 0;
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
isValid,
|
|
963
|
+
globalErrors,
|
|
964
|
+
siteValidations,
|
|
965
|
+
summary
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Test domain validation with known test cases
|
|
971
|
+
* @returns {boolean} True if all tests pass
|
|
972
|
+
*/
|
|
973
|
+
function testDomainValidation() {
|
|
974
|
+
const testCases = [
|
|
975
|
+
// Valid domains
|
|
976
|
+
{ domain: 'example.com', expected: true },
|
|
977
|
+
{ domain: 'sub.example.com', expected: true },
|
|
978
|
+
{ domain: 'test-site.co.uk', expected: true },
|
|
979
|
+
{ domain: '192.168.1.1', expected: true }, // IPv4
|
|
980
|
+
{ domain: '2001:db8::1', expected: true }, // IPv6
|
|
981
|
+
|
|
982
|
+
// Invalid domains
|
|
983
|
+
{ domain: '', expected: false },
|
|
984
|
+
{ domain: 'example', expected: false },
|
|
985
|
+
{ domain: '.example.com', expected: false },
|
|
986
|
+
{ domain: 'example.com.', expected: false },
|
|
987
|
+
{ domain: 'ex..ample.com', expected: false },
|
|
988
|
+
{ domain: '-example.com', expected: false }
|
|
989
|
+
];
|
|
990
|
+
|
|
991
|
+
let allPassed = true;
|
|
992
|
+
|
|
993
|
+
testCases.forEach(({ domain, expected }) => {
|
|
994
|
+
const result = isValidDomain(domain);
|
|
995
|
+
if (result !== expected) {
|
|
996
|
+
console.error(`Test failed for domain "${domain}": expected ${expected}, got ${result}`);
|
|
997
|
+
allPassed = false;
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
return allPassed;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Test adblock rule validation with known test cases
|
|
1006
|
+
* @returns {boolean} True if all tests pass
|
|
1007
|
+
*/
|
|
1008
|
+
function testAdblockValidation() {
|
|
1009
|
+
const testCases = [
|
|
1010
|
+
// Valid rules
|
|
1011
|
+
{ rule: '||example.com^', expected: true },
|
|
1012
|
+
{ rule: '||example.com^$script', expected: true },
|
|
1013
|
+
{ rule: '127.0.0.1 example.com', expected: true },
|
|
1014
|
+
{ rule: 'local=/example.com/', expected: true },
|
|
1015
|
+
|
|
1016
|
+
// Invalid rules
|
|
1017
|
+
{ rule: '', expected: false },
|
|
1018
|
+
{ rule: '||invalid..domain^', expected: false },
|
|
1019
|
+
{ rule: '||.example.com^', expected: false }
|
|
1020
|
+
];
|
|
1021
|
+
|
|
1022
|
+
let allPassed = true;
|
|
1023
|
+
|
|
1024
|
+
testCases.forEach(({ rule, expected }) => {
|
|
1025
|
+
const result = validateAdblockRule(rule);
|
|
1026
|
+
if (result.isValid !== expected) {
|
|
1027
|
+
console.error(`Test failed for rule "${rule}": expected ${expected}, got ${result.isValid}`);
|
|
1028
|
+
allPassed = false;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
return allPassed;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Validates a domain and formats it according to specified output options
|
|
1037
|
+
* @param {string} domain - The domain to validate and format
|
|
1038
|
+
* @param {object} options - Formatting options
|
|
1039
|
+
* @returns {string|object} Formatted domain string or error object
|
|
1040
|
+
*/
|
|
1041
|
+
function formatDomainWithValidation(domain, options = {}) {
|
|
1042
|
+
const {
|
|
1043
|
+
localhost = false,
|
|
1044
|
+
localhostAlt = false,
|
|
1045
|
+
plain = false,
|
|
1046
|
+
dnsmasq = false,
|
|
1047
|
+
dnsmasqOld = false,
|
|
1048
|
+
unbound = false,
|
|
1049
|
+
privoxy = false,
|
|
1050
|
+
pihole = false,
|
|
1051
|
+
adblockRules = false,
|
|
1052
|
+
resourceType = ''
|
|
1053
|
+
} = options;
|
|
1054
|
+
|
|
1055
|
+
// Validate domain first
|
|
1056
|
+
if (!isValidDomain(domain)) {
|
|
1057
|
+
return {
|
|
1058
|
+
isValid: false,
|
|
1059
|
+
error: `Invalid domain format: ${domain}`,
|
|
1060
|
+
formattedRule: null
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Format according to specified options (priority order)
|
|
1065
|
+
if (pihole) {
|
|
1066
|
+
const escapedDomain = domain.replace(/\./g, '\\.');
|
|
1067
|
+
return `(^|\\.)${escapedDomain}$`;
|
|
1068
|
+
} else if (privoxy) {
|
|
1069
|
+
return `{ +block } .${domain}`;
|
|
1070
|
+
} else if (dnsmasq) {
|
|
1071
|
+
return `local=/${domain}/`;
|
|
1072
|
+
} else if (dnsmasqOld) {
|
|
1073
|
+
return `server=/${domain}/`;
|
|
1074
|
+
} else if (unbound) {
|
|
1075
|
+
return `local-zone: "${domain}." always_null`;
|
|
1076
|
+
} else if (localhost) {
|
|
1077
|
+
return `127.0.0.1 ${domain}`;
|
|
1078
|
+
} else if (localhostAlt) {
|
|
1079
|
+
return `0.0.0.0 ${domain}`;
|
|
1080
|
+
} else if (adblockRules && resourceType) {
|
|
1081
|
+
return `||${domain}^$${resourceType}`;
|
|
1082
|
+
} else if (plain) {
|
|
1083
|
+
return domain;
|
|
1084
|
+
} else {
|
|
1085
|
+
// Default adblock format
|
|
1086
|
+
return `||${domain}^`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
module.exports = {
|
|
1091
|
+
isValidDomain,
|
|
1092
|
+
isValidDomainLabel,
|
|
1093
|
+
isValidTLD,
|
|
1094
|
+
isIPAddress,
|
|
1095
|
+
isIPv4,
|
|
1096
|
+
isIPv6,
|
|
1097
|
+
validateRegexPattern,
|
|
1098
|
+
validateAdblockModifiers,
|
|
1099
|
+
validateAdblockRule,
|
|
1100
|
+
validateRulesetFile,
|
|
1101
|
+
cleanRulesetFile,
|
|
1102
|
+
validateSiteConfig,
|
|
1103
|
+
validateFullConfig,
|
|
1104
|
+
testDomainValidation,
|
|
1105
|
+
testAdblockValidation,
|
|
1106
|
+
formatDomainWithValidation
|
|
1107
|
+
};
|