UNPKG

@fanboynz/network-scanner

Version:

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.

551 lines (484 loc) 15.9 kB
/** * Smart Cache Module - Intelligent multi-layer caching system for network scanner * Provides context-aware caching for domains, patterns, responses, and network tools */ const { LRUCache } = require('lru-cache'); const fs = require('fs'); const path = require('path'); const { formatLogMessage } = require('./colorize'); /** * SmartCache - Intelligent caching system with multiple cache layers * @class */ class SmartCache { constructor(options = {}) { this.options = { maxSize: options.maxSize || 5000, ttl: options.ttl || 1000 * 60 * 60, // 1 hour default enablePatternCache: options.enablePatternCache !== false, enableResponseCache: options.enableResponseCache !== false, enableWhoisCache: options.enableWhoisCache !== false, enablePersistence: options.enablePersistence === true, persistencePath: options.persistencePath || '.cache', forceDebug: options.forceDebug || false, autoSave: options.autoSave !== false, autoSaveInterval: options.autoSaveInterval || 60000 // 1 minute }; // Initialize cache layers this._initializeCaches(); // Initialize statistics this._initializeStats(); // Load persistent cache if enabled if (this.options.enablePersistence) { this._loadPersistentCache(); } // Set up auto-save if enabled if (this.options.enablePersistence && this.options.autoSave) { this._setupAutoSave(); } } /** * Initialize all cache layers * @private */ _initializeCaches() { // Domain detection cache with TTL this.domainCache = new LRUCache({ max: this.options.maxSize, ttl: this.options.ttl, updateAgeOnGet: true, updateAgeOnHas: false }); // Pattern matching results cache this.patternCache = new LRUCache({ max: 1000, ttl: this.options.ttl * 2 // Patterns are more stable }); // Response content cache for searchstring operations this.responseCache = new LRUCache({ max: 200, ttl: 1000 * 60 * 30, // 30 minutes for response content maxSize: 50 * 1024 * 1024, // 50MB max cache size sizeCalculation: (value) => value.length }); // WHOIS/DNS results cache this.netToolsCache = new LRUCache({ max: 500, ttl: 1000 * 60 * 60 * 24 // 24 hours for WHOIS/DNS }); // Similarity cache for expensive string comparisons this.similarityCache = new LRUCache({ max: 2000, ttl: this.options.ttl }); // Regex compilation cache this.regexCache = new Map(); } /** * Initialize statistics tracking * @private */ _initializeStats() { this.stats = { hits: 0, misses: 0, patternHits: 0, patternMisses: 0, responseHits: 0, responseMisses: 0, netToolsHits: 0, netToolsMisses: 0, similarityHits: 0, similarityMisses: 0, regexCompilations: 0, regexCacheHits: 0, persistenceLoads: 0, persistenceSaves: 0, startTime: Date.now() }; } /** * Check if domain should be skipped based on smart caching * @param {string} domain - Domain to check * @param {Object} context - Processing context * @returns {boolean} True if domain should be skipped */ shouldSkipDomain(domain, context = {}) { const cacheKey = this._generateCacheKey(domain, context); if (this.domainCache.has(cacheKey)) { this.stats.hits++; if (this.options.forceDebug) { const cached = this.domainCache.get(cacheKey); const age = Date.now() - cached.timestamp; console.log(formatLogMessage('debug', `[SmartCache] Cache hit for ${domain} (age: ${Math.round(age/1000)}s, context: ${JSON.stringify(context)})` )); } return true; } this.stats.misses++; return false; } /** * Mark domain as processed with context * @param {string} domain - Domain to mark * @param {Object} context - Processing context * @param {Object} metadata - Additional metadata to store */ markDomainProcessed(domain, context = {}, metadata = {}) { const cacheKey = this._generateCacheKey(domain, context); this.domainCache.set(cacheKey, { timestamp: Date.now(), metadata, context, domain }); if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Marked ${domain} as processed (context: ${JSON.stringify(context)})` )); } } /** * Generate cache key with context awareness * @param {string} domain - Domain * @param {Object} context - Context object * @returns {string} Cache key * @private */ _generateCacheKey(domain, context) { const { filterRegex, searchString, resourceType, nettools } = context; const components = [ domain, filterRegex || '', searchString || '', resourceType || '', nettools ? 'nt' : '' ].filter(Boolean); return components.join(':'); } /** * Get or compile regex pattern with caching * @param {string} pattern - Regex pattern string * @returns {RegExp} Compiled regex */ getCompiledRegex(pattern) { if (!this.regexCache.has(pattern)) { this.stats.regexCompilations++; try { const regex = new RegExp(pattern.replace(/^\/(.*)\/$/, '$1')); this.regexCache.set(pattern, regex); } catch (err) { if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Failed to compile regex: ${pattern}` )); } return null; } } else { this.stats.regexCacheHits++; } return this.regexCache.get(pattern); } /** * Check pattern matching cache * @param {string} url - URL to check * @param {string} pattern - Regex pattern * @returns {boolean|null} Cached result or null if not cached */ getCachedPatternMatch(url, pattern) { if (!this.options.enablePatternCache) return null; const cacheKey = `${url}:${pattern}`; const cached = this.patternCache.get(cacheKey); if (cached !== undefined) { this.stats.patternHits++; if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Pattern cache hit for ${url.substring(0, 50)}...` )); } return cached; } this.stats.patternMisses++; return null; } /** * Cache pattern matching result * @param {string} url - URL * @param {string} pattern - Regex pattern * @param {boolean} result - Match result */ cachePatternMatch(url, pattern, result) { if (!this.options.enablePatternCache) return; const cacheKey = `${url}:${pattern}`; this.patternCache.set(cacheKey, result); } /** * Get cached response content * @param {string} url - URL * @returns {string|null} Cached content or null */ getCachedResponse(url) { if (!this.options.enableResponseCache) return null; const cached = this.responseCache.get(url); if (cached) { this.stats.responseHits++; if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Response cache hit for ${url.substring(0, 50)}...` )); } return cached; } this.stats.responseMisses++; return null; } /** * Cache response content * @param {string} url - URL * @param {string} content - Response content */ cacheResponse(url, content) { if (!this.options.enableResponseCache) return; // Only cache if content is reasonable size if (content && content.length < 5 * 1024 * 1024) { // 5MB limit per response this.responseCache.set(url, content); } } /** * Get cached WHOIS/DNS results * @param {string} domain - Domain * @param {string} tool - Tool name (whois/dig) * @param {string} recordType - Record type for dig * @returns {Object|null} Cached result or null */ getCachedNetTools(domain, tool, recordType = null) { if (!this.options.enableWhoisCache) return null; const cacheKey = `${tool}:${domain}${recordType ? ':' + recordType : ''}`; const cached = this.netToolsCache.get(cacheKey); if (cached) { this.stats.netToolsHits++; if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] ${tool.toUpperCase()} cache hit for ${domain}` )); } return cached; } this.stats.netToolsMisses++; return null; } /** * Cache WHOIS/DNS results * @param {string} domain - Domain * @param {string} tool - Tool name * @param {Object} result - Result to cache * @param {string} recordType - Record type for dig */ cacheNetTools(domain, tool, result, recordType = null) { if (!this.options.enableWhoisCache) return; const cacheKey = `${tool}:${domain}${recordType ? ':' + recordType : ''}`; this.netToolsCache.set(cacheKey, result); } /** * Cache similarity comparison result * @param {string} domain1 - First domain * @param {string} domain2 - Second domain * @param {number} similarity - Similarity score */ cacheSimilarity(domain1, domain2, similarity) { const key = [domain1, domain2].sort().join('|'); this.similarityCache.set(key, similarity); } /** * Get cached similarity score * @param {string} domain1 - First domain * @param {string} domain2 - Second domain * @returns {number|null} Cached similarity or null */ getCachedSimilarity(domain1, domain2) { const key = [domain1, domain2].sort().join('|'); const cached = this.similarityCache.get(key); if (cached !== undefined) { this.stats.similarityHits++; return cached; } this.stats.similarityMisses++; return null; } /** * Get cache statistics * @returns {Object} Statistics object */ getStats() { const runtime = Date.now() - this.stats.startTime; const hitRate = this.stats.hits / (this.stats.hits + this.stats.misses) || 0; const patternHitRate = this.stats.patternHits / (this.stats.patternHits + this.stats.patternMisses) || 0; const responseHitRate = this.stats.responseHits / (this.stats.responseHits + this.stats.responseMisses) || 0; const netToolsHitRate = this.stats.netToolsHits / (this.stats.netToolsHits + this.stats.netToolsMisses) || 0; return { ...this.stats, runtime: Math.round(runtime / 1000), // seconds hitRate: (hitRate * 100).toFixed(2) + '%', patternHitRate: (patternHitRate * 100).toFixed(2) + '%', responseHitRate: (responseHitRate * 100).toFixed(2) + '%', netToolsHitRate: (netToolsHitRate * 100).toFixed(2) + '%', domainCacheSize: this.domainCache.size, patternCacheSize: this.patternCache.size, responseCacheSize: this.responseCache.size, netToolsCacheSize: this.netToolsCache.size, similarityCacheSize: this.similarityCache.size, regexCacheSize: this.regexCache.size, totalCacheEntries: this.domainCache.size + this.patternCache.size + this.responseCache.size + this.netToolsCache.size + this.similarityCache.size + this.regexCache.size }; } /** * Clear all caches */ clear() { this.domainCache.clear(); this.patternCache.clear(); this.responseCache.clear(); this.netToolsCache.clear(); this.similarityCache.clear(); this.regexCache.clear(); this._initializeStats(); if (this.options.forceDebug) { console.log(formatLogMessage('debug', '[SmartCache] All caches cleared')); } } /** * Load persistent cache from disk * @private */ _loadPersistentCache() { const cacheFile = path.join(this.options.persistencePath, 'smart-cache.json'); if (!fs.existsSync(cacheFile)) { return; } try { const data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); const now = Date.now(); // Validate cache age if (data.timestamp && now - data.timestamp > 24 * 60 * 60 * 1000) { if (this.options.forceDebug) { console.log(formatLogMessage('debug', '[SmartCache] Persistent cache too old, ignoring' )); } return; } // Load domain cache if (data.domainCache && Array.isArray(data.domainCache)) { data.domainCache.forEach(([key, value]) => { // Only load if not expired if (now - value.timestamp < this.options.ttl) { this.domainCache.set(key, value); } }); } // Load nettools cache if (data.netToolsCache && Array.isArray(data.netToolsCache)) { data.netToolsCache.forEach(([key, value]) => { this.netToolsCache.set(key, value); }); } this.stats.persistenceLoads++; if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Loaded persistent cache: ${this.domainCache.size} domains, ${this.netToolsCache.size} nettools` )); } } catch (err) { if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Failed to load persistent cache: ${err.message}` )); } } } /** * Save cache to disk */ savePersistentCache() { if (!this.options.enablePersistence) return; const cacheDir = this.options.persistencePath; const cacheFile = path.join(cacheDir, 'smart-cache.json'); try { // Create cache directory if it doesn't exist if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } const data = { timestamp: Date.now(), domainCache: Array.from(this.domainCache.entries()), netToolsCache: Array.from(this.netToolsCache.entries()), stats: this.stats }; fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2)); this.stats.persistenceSaves++; if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Saved cache to disk: ${cacheFile}` )); } } catch (err) { if (this.options.forceDebug) { console.log(formatLogMessage('debug', `[SmartCache] Failed to save cache: ${err.message}` )); } } } /** * Set up auto-save interval * @private */ _setupAutoSave() { this.autoSaveInterval = setInterval(() => { this.savePersistentCache(); }, this.options.autoSaveInterval); } /** * Clean up resources */ destroy() { if (this.autoSaveInterval) { clearInterval(this.autoSaveInterval); } // Save cache one last time if (this.options.enablePersistence) { this.savePersistentCache(); } this.clear(); } } /** * Factory function to create SmartCache instance with config * @param {Object} config - Configuration object * @returns {SmartCache} SmartCache instance */ function createSmartCache(config = {}) { return new SmartCache({ maxSize: config.cache_max_size, ttl: (config.cache_ttl_minutes || 60) * 60 * 1000, enablePatternCache: config.cache_patterns !== false, enableResponseCache: config.cache_responses !== false, enableWhoisCache: config.cache_nettools !== false, enablePersistence: config.cache_persistence === true, persistencePath: config.cache_path || '.cache', forceDebug: config.forceDebug || false, autoSave: config.cache_autosave !== false, autoSaveInterval: (config.cache_autosave_minutes || 1) * 60 * 1000 }); } module.exports = { SmartCache, createSmartCache };