@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.
@@ -11,7 +11,8 @@
11
11
  const { execSync, spawn } = require('child_process');
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const { formatLogMessage } = require('./colorize');
14
+ const { formatLogMessage, messageColors } = require('./colorize');
15
+ const OPENVPN_TAG = messageColors.processing('[openvpn]');
15
16
 
16
17
  /**
17
18
  * Fetch external IP address through the active tunnel
@@ -263,7 +264,7 @@ function waitForConnection(child, logPath, timeout, forceDebug) {
263
264
  const tunDevice = findTunDevice(child.pid);
264
265
  if (forceDebug) {
265
266
  console.log(formatLogMessage('debug',
266
- `[openvpn] Connected (tun: ${tunDevice || 'unknown'}, ${Date.now() - startTime}ms)`
267
+ `${OPENVPN_TAG} Connected (tun: ${tunDevice || 'unknown'}, ${Date.now() - startTime}ms)`
267
268
  ));
268
269
  }
269
270
  done({ connected: true, tunDevice });
@@ -352,7 +353,7 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
352
353
 
353
354
  if (activeConnections.has(connectionName)) {
354
355
  if (forceDebug) {
355
- console.log(formatLogMessage('debug', `[openvpn] ${connectionName} already active`));
356
+ console.log(formatLogMessage('debug', `${OPENVPN_TAG} ${connectionName} already active`));
356
357
  }
357
358
  const existing = activeConnections.get(connectionName);
358
359
  return { success: true, connection: connectionName, tunDevice: existing.tunDevice, alreadyActive: true };
@@ -379,7 +380,7 @@ async function startConnection(configPath, vpnConfig, forceDebug = false) {
379
380
  const args = buildArgs(configPath, vpnConfig, connectionName, logPath);
380
381
 
381
382
  if (forceDebug) {
382
- console.log(formatLogMessage('debug', `[openvpn] Starting: openvpn ${args.join(' ')}`));
383
+ console.log(formatLogMessage('debug', `${OPENVPN_TAG} Starting: openvpn ${args.join(' ')}`));
383
384
  }
384
385
 
385
386
  // Spawn OpenVPN — it daemonizes itself via --daemon, but we spawn
@@ -458,7 +459,7 @@ function stopConnection(connectionName, forceDebug = false) {
458
459
  } catch (killErr) {
459
460
  // Process may already be dead
460
461
  if (forceDebug) {
461
- console.log(formatLogMessage('debug', `[openvpn] Kill error (may be already dead): ${killErr.message}`));
462
+ console.log(formatLogMessage('debug', `${OPENVPN_TAG} Kill error (may be already dead): ${killErr.message}`));
462
463
  }
463
464
  }
464
465
 
@@ -468,7 +469,7 @@ function stopConnection(connectionName, forceDebug = false) {
468
469
  cleanupConnectionFiles(connectionName);
469
470
 
470
471
  if (forceDebug) {
471
- console.log(formatLogMessage('debug', `[openvpn] ${connectionName} stopped`));
472
+ console.log(formatLogMessage('debug', `${OPENVPN_TAG} ${connectionName} stopped`));
472
473
  }
473
474
 
474
475
  return { success: true };
@@ -544,7 +545,7 @@ function checkConnection(connectionName, testHost = '1.1.1.1', forceDebug = fals
544
545
 
545
546
  if (forceDebug) {
546
547
  console.log(formatLogMessage('debug',
547
- `[openvpn] ${connectionName} connected (${latencyMs ? latencyMs + 'ms' : 'ok'})`
548
+ `${OPENVPN_TAG} ${connectionName} connected (${latencyMs ? latencyMs + 'ms' : 'ok'})`
548
549
  ));
549
550
  }
550
551
 
@@ -552,7 +553,7 @@ function checkConnection(connectionName, testHost = '1.1.1.1', forceDebug = fals
552
553
  } catch (error) {
553
554
  if (forceDebug) {
554
555
  console.log(formatLogMessage('debug',
555
- `[openvpn] ${connectionName} health check failed: ${error.message.split('\n')[0]}`
556
+ `${OPENVPN_TAG} ${connectionName} health check failed: ${error.message.split('\n')[0]}`
556
557
  ));
557
558
  }
558
559
  return { connected: false, error: error.message.split('\n')[0] };
@@ -756,7 +757,7 @@ async function connectForSite(siteConfig, forceDebug = false) {
756
757
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
757
758
  if (forceDebug && attempt > 1) {
758
759
  console.log(formatLogMessage('debug',
759
- `[openvpn] Retry ${attempt - 1}/${ovpnConfig.max_retries} for ${connectionName}`
760
+ `${OPENVPN_TAG} Retry ${attempt - 1}/${ovpnConfig.max_retries} for ${connectionName}`
760
761
  ));
761
762
  }
762
763
 
@@ -833,7 +834,7 @@ function disconnectForSite(siteConfig, forceDebug = false) {
833
834
 
834
835
  if (forceDebug) {
835
836
  console.log(formatLogMessage('debug',
836
- `[openvpn] ${connectionName} still used by ${info.sites.size} site(s), keeping up`
837
+ `${OPENVPN_TAG} ${connectionName} still used by ${info.sites.size} site(s), keeping up`
837
838
  ));
838
839
  }
839
840
 
@@ -863,7 +864,7 @@ function disconnectAll(forceDebug = false) {
863
864
 
864
865
  if (forceDebug && results.tornDown > 0) {
865
866
  console.log(formatLogMessage('debug',
866
- `[openvpn] Disconnected ${results.tornDown} connection(s)`
867
+ `${OPENVPN_TAG} Disconnected ${results.tornDown} connection(s)`
867
868
  ));
868
869
  }
869
870
 
package/lib/output.js CHANGED
@@ -1,9 +1,8 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- // Import domain cache functions for statistics
4
- const { getTotalDomainsSkipped } = require('./domain-cache');
5
3
  const { loadComparisonRules, filterUniqueRules } = require('./compare');
6
- const { colorize, colors, messageColors, tags, formatLogMessage } = require('./colorize');
4
+ const { messageColors, formatLogMessage } = require('./colorize');
5
+ const OUTPUT_FILTER_TAG = messageColors.processing('[output-filter]');
7
6
 
8
7
  // Cache for compiled wildcard regex patterns in matchesIgnoreDomain (capped to prevent memory leak)
9
8
  const wildcardRegexCache = new Map();
@@ -40,15 +39,28 @@ function matchesIgnoreDomain(domain, ignorePatterns) {
40
39
  const baseDomain = pattern.slice(0, -2); // Remove ".*"
41
40
  return domain.startsWith(baseDomain + '.');
42
41
  } else {
43
- // Complex wildcard pattern (cached)
42
+ // Complex wildcard pattern (cached). Escape every regex meta-char
43
+ // EXCEPT '*' first, then expand '*' to '.*'. The old code only
44
+ // escaped '.', so a pattern containing '+', '(', '[', '?', etc.
45
+ // would either misbehave (e.g. 'foo+bar.com' would treat '+' as a
46
+ // quantifier) or throw synchronously (unmatched '(' / '[') and the
47
+ // exception would propagate out of .some(). Domain names can't
48
+ // legally contain those chars, but a typo in a user's ignore list
49
+ // would crash the output stage.
44
50
  if (!wildcardRegexCache.has(pattern)) {
45
51
  if (wildcardRegexCache.size >= WILDCARD_CACHE_MAX) {
46
52
  wildcardRegexCache.delete(wildcardRegexCache.keys().next().value);
47
53
  }
48
54
  const regexPattern = pattern
49
- .replace(/\./g, '\\.') // Escape dots
50
- .replace(/\*/g, '.*'); // Convert * to .*
51
- wildcardRegexCache.set(pattern, new RegExp(`^${regexPattern}$`));
55
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape all regex meta-chars except '*'
56
+ .replace(/\*/g, '.*'); // Now expand '*' to '.*'
57
+ try {
58
+ wildcardRegexCache.set(pattern, new RegExp(`^${regexPattern}$`));
59
+ } catch (_) {
60
+ // Defensive: a still-malformed regex (shouldn't happen after the
61
+ // escape above) becomes a never-match instead of a crash.
62
+ wildcardRegexCache.set(pattern, /(?!)/);
63
+ }
52
64
  }
53
65
  return wildcardRegexCache.get(pattern).test(domain);
54
66
  }
@@ -389,7 +401,7 @@ function buildOutputLines(results, options = {}) {
389
401
  if (domain && matchesIgnoreDomain(domain, ignoreDomains)) {
390
402
  filteredOutCount++;
391
403
  if (options.forceDebug) {
392
- console.log(formatLogMessage('debug', `[output-filter] Removed rule matching ignoreDomains: ${rule} (domain: ${domain})`));
404
+ console.log(formatLogMessage('debug', `${OUTPUT_FILTER_TAG} Removed rule matching ignoreDomains: ${rule} (domain: ${domain})`));
393
405
  } else if (!options.silentMode) {
394
406
  console.log(formatLogMessage('info', `Filtered out: ${domain}`));
395
407
  }
@@ -409,7 +421,7 @@ function buildOutputLines(results, options = {}) {
409
421
  // Log filtered domains if any were removed
410
422
  if (filteredOutCount > 0) {
411
423
  if (options.forceDebug) {
412
- console.log(formatLogMessage('debug', `[output-filter] Total: ${filteredOutCount} rules filtered out matching ignoreDomains patterns`));
424
+ console.log(formatLogMessage('debug', `${OUTPUT_FILTER_TAG} Total: ${filteredOutCount} rules filtered out matching ignoreDomains patterns`));
413
425
  } else if (!options.silentMode) {
414
426
  console.log(formatLogMessage('info', `${filteredOutCount} domains filtered out by ignoreDomains`));
415
427
  }
@@ -456,7 +468,7 @@ function writeOutput(lines, outputFile = null, silentMode = false) {
456
468
  }
457
469
  return true;
458
470
  } catch (error) {
459
- console.error(`? Failed to write output: ${error.message}`);
471
+ console.error(`Failed to write output: ${error.message}`);
460
472
  return false;
461
473
  }
462
474
  }
@@ -478,39 +490,41 @@ function handleOutput(results, config = {}) {
478
490
  dumpUrls = false,
479
491
  adblockRulesLogFile = null,
480
492
  forceDebug = false,
481
- ignoreDomains = [],
482
- totalDomainsSkipped = null // Allow override or get from cache
493
+ ignoreDomains = []
483
494
  } = config;
484
495
 
485
496
  // Handle append mode
486
497
  if (outputFile && appendMode) {
487
498
  try {
488
- // Build output lines first
489
- const {
490
- outputLines,
491
- outputLinesWithTitles,
499
+ // Build output lines first. buildOutputLines already applies
500
+ // removeDuplicates internally when removeDupes is true, so we don't
501
+ // need a second pass here.
502
+ const {
503
+ outputLines,
504
+ outputLinesWithTitles,
492
505
  successfulPageLoads,
493
506
  totalRules,
494
507
  filteredOutCount
495
- } = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains: config.ignoreDomains, forceDebug: config.forceDebug });
496
-
497
- // Apply remove-dupes to new results if requested (before comparing to existing file)
498
- const deduplicatedOutputLines = removeDupes ? removeDuplicates(outputLines) : outputLines;
499
- if (removeDupes && forceDebug) console.log(formatLogMessage('debug', `Applied --remove-dupes to new scan results before append comparison`));
508
+ } = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains, forceDebug });
509
+ const deduplicatedOutputLines = outputLines;
500
510
 
501
- // Read existing file content
511
+ // Read existing file content via a single open() instead of stat+open
512
+ // (and avoid TOCTOU between an existsSync check and the read).
502
513
  let existingContent = '';
503
- if (fs.existsSync(outputFile)) {
514
+ try {
504
515
  existingContent = fs.readFileSync(outputFile, 'utf8');
505
- } else {
516
+ } catch (readErr) {
517
+ if (readErr.code !== 'ENOENT') throw readErr;
506
518
  // File doesn't exist - append mode should create it
507
519
  if (forceDebug) console.log(formatLogMessage('debug', `Append mode: Creating new file ${outputFile}`));
508
520
  }
509
-
510
- // Parse existing rules for comparison (exclude comments)
521
+
522
+ // Parse existing rules for comparison (exclude comments). Hoist the
523
+ // single .trim() into a local so we don't walk the file content twice.
511
524
  const existingRules = new Set();
512
- if (existingContent.trim()) {
513
- const lines = existingContent.trim().split('\n');
525
+ const trimmedExisting = existingContent.trim();
526
+ if (trimmedExisting) {
527
+ const lines = trimmedExisting.split('\n');
514
528
  lines.forEach(line => {
515
529
  const cleanLine = line.trim();
516
530
  if (cleanLine && !cleanLine.startsWith('!') && !cleanLine.startsWith('#')) {
@@ -524,39 +538,47 @@ function handleOutput(results, config = {}) {
524
538
  return rule.startsWith('!') || !existingRules.has(rule);
525
539
  });
526
540
 
541
+ // Count non-comment rules once and reuse below (was three throwaway
542
+ // filter-array allocations: success log, else-branch log, return obj).
543
+ let newRuleCount = 0;
544
+ for (let i = 0; i < newRules.length; i++) {
545
+ if (!newRules[i].startsWith('!')) newRuleCount++;
546
+ }
547
+
527
548
  if (newRules.length > 0) {
528
549
  // Prepare content to append
529
550
  let appendContent = '';
530
-
551
+
531
552
  // Ensure there's a newline before appending if file has content
532
553
  if (existingContent && !existingContent.endsWith('\n')) {
533
554
  appendContent = '\n';
534
555
  }
535
-
556
+
536
557
  // Add new rules
537
558
  appendContent += newRules.join('\n') + '\n';
538
-
559
+
539
560
  // Append to file
540
561
  fs.appendFileSync(outputFile, appendContent);
541
-
542
- const newRuleCount = newRules.filter(rule => !rule.startsWith('!')).length;
562
+
543
563
  if (!silentMode) {
544
- console.log(`${messageColors.success('? Appended')} ${newRuleCount} new rules to: ${outputFile} (${existingRules.size} rules already existed${removeDupes ? ', duplicates removed' : ''})`);
564
+ console.log(`${messageColors.success('Appended')} ${newRuleCount} new rules to: ${outputFile} (${existingRules.size} rules already existed${removeDupes ? ', duplicates removed' : ''})`);
545
565
  }
546
- } else {
547
- if (!silentMode) {
548
- const ruleCount = deduplicatedOutputLines.filter(rule => !rule.startsWith('!')).length;
549
- console.log(`${messageColors.info('?')} No new rules to append - all ${ruleCount} rules already exist in: ${outputFile}`);
566
+ } else if (!silentMode) {
567
+ // No new rules — report the dedup'd input count instead. Same loop
568
+ // pattern as above to avoid filter().length allocating an array.
569
+ let ruleCount = 0;
570
+ for (let i = 0; i < deduplicatedOutputLines.length; i++) {
571
+ if (!deduplicatedOutputLines[i].startsWith('!')) ruleCount++;
550
572
  }
573
+ console.log(`${messageColors.info('No new rules')} to append - all ${ruleCount} rules already exist in: ${outputFile}`);
551
574
  }
552
-
575
+
553
576
  // Write log file output if --dumpurls is enabled
554
577
  let logSuccess = true;
555
578
  if (dumpUrls && adblockRulesLogFile) {
556
579
  logSuccess = writeOutput(outputLinesWithTitles, adblockRulesLogFile, silentMode);
557
580
  }
558
-
559
- const newRuleCount = newRules.filter(rule => !rule.startsWith('!')).length;
581
+
560
582
  return {
561
583
  success: logSuccess,
562
584
  outputFile,
@@ -571,34 +593,42 @@ function handleOutput(results, config = {}) {
571
593
  };
572
594
 
573
595
  } catch (appendErr) {
574
- console.error(`? Failed to append to ${outputFile}: ${appendErr.message}`);
596
+ console.error(`Failed to append to ${outputFile}: ${appendErr.message}`);
575
597
  return { success: false };
576
598
  }
577
599
  }
578
600
 
579
601
  // Build output lines
580
- const {
581
- outputLines,
582
- outputLinesWithTitles,
602
+ const {
603
+ outputLines,
604
+ outputLinesWithTitles,
583
605
  successfulPageLoads,
584
606
  totalRules,
585
607
  filteredOutCount
586
- } = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains: config.ignoreDomains, forceDebug: config.forceDebug });
608
+ } = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains, forceDebug });
587
609
 
588
610
  // Apply comparison filtering if compareFile is specified
589
611
  let filteredOutputLines = outputLines;
590
612
  if (compareFile && outputLines.length > 0) {
591
613
  try {
592
- const comparisonRules = loadComparisonRules(compareFile, config.forceDebug);
593
- const originalCount = outputLines.filter(line => !line.startsWith('!')).length;
594
- filteredOutputLines = filterUniqueRules(outputLines, comparisonRules, config.forceDebug);
595
-
596
- if (!silentMode) {
597
- console.log(formatLogMessage('compare', `Filtered ${originalCount - filteredOutputLines.filter(line => !line.startsWith('!')).length} existing rules, ${filteredOutputLines.filter(line => !line.startsWith('!')).length} unique rules remaining`));
614
+ const comparisonRules = loadComparisonRules(compareFile, forceDebug);
615
+ // Count non-comment lines once each side instead of building filter
616
+ // arrays just to read .length (was three allocations per log line).
617
+ let originalCount = 0;
618
+ for (let i = 0; i < outputLines.length; i++) {
619
+ if (!outputLines[i].startsWith('!')) originalCount++;
620
+ }
621
+ filteredOutputLines = filterUniqueRules(outputLines, comparisonRules, forceDebug);
598
622
 
623
+ if (!silentMode) {
624
+ let uniqueCount = 0;
625
+ for (let i = 0; i < filteredOutputLines.length; i++) {
626
+ if (!filteredOutputLines[i].startsWith('!')) uniqueCount++;
627
+ }
628
+ console.log(formatLogMessage('compare', `Filtered ${originalCount - uniqueCount} existing rules, ${uniqueCount} unique rules remaining`));
599
629
  }
600
630
  } catch (compareError) {
601
- console.error(messageColors.error('Compare operation failed:') + ` ${compareError.message}`);
631
+ console.error(messageColors.error('Compare operation failed:') + ` ${compareError.message}`);
602
632
  return { success: false, totalRules: 0, successfulPageLoads: 0 };
603
633
  }
604
634
  }
@@ -612,21 +642,24 @@ function handleOutput(results, config = {}) {
612
642
  logSuccess = writeOutput(outputLinesWithTitles, adblockRulesLogFile, silentMode);
613
643
  }
614
644
 
615
- // Get domain skip statistics from cache if not provided
616
- const finalTotalDomainsSkipped = totalDomainsSkipped !== null ?
617
- totalDomainsSkipped : getTotalDomainsSkipped();
645
+ // Count non-comment lines once (used by totalRules below). Doing this with a
646
+ // single loop avoids the .filter().length pattern that allocates a throwaway
647
+ // array. Callers that want totalDomainsSkipped should call
648
+ // getTotalDomainsSkipped() from ./domain-cache directly.
649
+ let finalRuleCount = 0;
650
+ for (let i = 0; i < filteredOutputLines.length; i++) {
651
+ if (!filteredOutputLines[i].startsWith('!')) finalRuleCount++;
652
+ }
618
653
 
619
654
  return {
620
655
  success: mainSuccess && logSuccess,
621
656
  outputFile,
622
657
  adblockRulesLogFile,
623
658
  successfulPageLoads,
624
- totalRules: filteredOutputLines.filter(line => !line.startsWith('!')).length,
659
+ totalRules: finalRuleCount,
625
660
  filteredOutCount,
626
661
  totalLines: filteredOutputLines.length,
627
662
  outputLines: outputFile ? null : filteredOutputLines // Only return lines if not written to file
628
- // Note: totalDomainsSkipped statistic is now available via getTotalDomainsSkipped()
629
- // and doesn't need to be passed through the output handler
630
663
  };
631
664
  }
632
665