/** * 代理工具模块 * 提供代理控制器的公共函数和常量 * @module proxy-util */ import {ENV} from './env.js'; import https from 'https'; import http from 'http'; import {URL} from 'url'; /** * 代理相关常量 */ export const PROXY_CONSTANTS = { // 缓存超时时间(5分钟) CACHE_TIMEOUT: 5 * 60 * 1000, // M3U8 缓存超时时间(30秒,因为直播流更新频繁) M3U8_CACHE_TIMEOUT: 30 * 1000, // 请求超时时间(30秒) REQUEST_TIMEOUT: 30000, // HEAD请求超时时间(5秒,用于快速检测) HEAD_REQUEST_TIMEOUT: 5000, // 内容获取超时时间(15秒) CONTENT_FETCH_TIMEOUT: 15000, // 最大内容长度(10MB) MAX_CONTENT_LENGTH: 10 * 1024 * 1024, // 默认User-Agent DEFAULT_USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', // 缓存管理器配置 CACHE_MANAGER: { // 默认最大缓存条目数 DEFAULT_MAX_SIZE: 1000, // 默认过期时间(5分钟) DEFAULT_TTL: 5 * 60 * 1000, // M3U8缓存过期时间(30秒) M3U8_TTL: 30 * 1000, // 清理检查间隔(1分钟) CLEANUP_INTERVAL: 60 * 1000, // 内存压力阈值(当缓存条目超过此数量时触发积极清理) MEMORY_PRESSURE_THRESHOLD: 5000 } }; /** * 智能缓存管理器 * 提供LRU淘汰、自动过期、内存监控等功能,防止内存泄漏 */ export class SmartCacheManager { constructor(options = {}) { this.maxSize = options.maxSize || PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_MAX_SIZE; this.defaultTTL = options.defaultTTL || PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_TTL; this.cleanupInterval = options.cleanupInterval || PROXY_CONSTANTS.CACHE_MANAGER.CLEANUP_INTERVAL; this.name = options.name || 'SmartCache'; // 缓存存储:key -> {value, timestamp, accessTime, ttl} this.cache = new Map(); // 访问顺序链表(用于LRU) this.accessOrder = new Map(); // 统计信息 this.stats = { hits: 0, misses: 0, evictions: 0, expirations: 0, cleanups: 0 }; // 启动定期清理任务 this.startCleanupTimer(); console.log(`[${this.name}] SmartCacheManager initialized: maxSize=${this.maxSize}, defaultTTL=${this.defaultTTL}ms`); } /** * 设置缓存项 * @param {string} key - 缓存键 * @param {*} value - 缓存值 * @param {number} ttl - 过期时间(毫秒),可选 */ set(key, value, ttl = null) { const now = Date.now(); const itemTTL = ttl || this.defaultTTL; // 如果已存在,先删除旧的 if (this.cache.has(key)) { this.accessOrder.delete(key); } // 检查是否需要淘汰 this._evictIfNeeded(); // 添加新项 const item = { value, timestamp: now, accessTime: now, ttl: itemTTL, expiresAt: now + itemTTL }; this.cache.set(key, item); this.accessOrder.set(key, now); // 内存压力检查 if (this.cache.size > PROXY_CONSTANTS.CACHE_MANAGER.MEMORY_PRESSURE_THRESHOLD) { console.warn(`[${this.name}] Memory pressure detected: ${this.cache.size} items, triggering aggressive cleanup`); this._aggressiveCleanup(); } } /** * 获取缓存项 * @param {string} key - 缓存键 * @returns {*} 缓存值,如果不存在或已过期返回null */ get(key) { const item = this.cache.get(key); if (!item) { this.stats.misses++; return null; } const now = Date.now(); // 检查是否过期 if (now > item.expiresAt) { this.cache.delete(key); this.accessOrder.delete(key); this.stats.misses++; this.stats.expirations++; return null; } // 更新访问时间(LRU) item.accessTime = now; this.accessOrder.delete(key); this.accessOrder.set(key, now); this.stats.hits++; return item.value; } /** * 检查缓存项是否存在且未过期 * @param {string} key - 缓存键 * @returns {boolean} */ has(key) { return this.get(key) !== null; } /** * 删除缓存项 * @param {string} key - 缓存键 * @returns {boolean} 是否成功删除 */ delete(key) { const deleted = this.cache.delete(key); if (deleted) { this.accessOrder.delete(key); } return deleted; } /** * 清空所有缓存 */ clear() { const size = this.cache.size; this.cache.clear(); this.accessOrder.clear(); console.log(`[${this.name}] Cache cleared: ${size} items removed`); } /** * 获取缓存大小 * @returns {number} */ get size() { return this.cache.size; } /** * 获取统计信息 * @returns {Object} */ getStats() { const hitRate = this.stats.hits + this.stats.misses > 0 ? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2) : '0.00'; return { hits: this.stats.hits, misses: this.stats.misses, evictions: this.stats.evictions, expirations: this.stats.expirations, cleanups: this.stats.cleanups, size: this.cache.size, maxSize: this.maxSize, hitRate: `${hitRate}%`, memoryUsage: this._estimateMemoryUsage(), name: this.name, defaultTTL: this.defaultTTL, cleanupInterval: this.cleanupInterval }; } /** * 启动定期清理任务 */ startCleanupTimer() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { this._performCleanup(); }, this.cleanupInterval); // 确保进程退出时清理定时器 if (typeof process !== 'undefined') { process.on('exit', () => this.stopCleanupTimer()); } } /** * 停止定期清理任务 */ stopCleanupTimer() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } } /** * 执行清理任务 */ _performCleanup() { const before = this.cache.size; const now = Date.now(); let expiredCount = 0; // 清理过期项 for (const [key, item] of this.cache.entries()) { if (now > item.expiresAt) { this.cache.delete(key); this.accessOrder.delete(key); expiredCount++; } } if (expiredCount > 0) { this.stats.expirations += expiredCount; this.stats.cleanups++; console.log(`[${this.name}] Cleanup completed: ${expiredCount} expired items removed (${before} -> ${this.cache.size})`); } } /** * 积极清理(内存压力时) */ _aggressiveCleanup() { const targetSize = Math.floor(this.maxSize * 0.7); // 清理到70%容量 let removedCount = 0; // 首先清理过期项 this._performCleanup(); // 如果还是太多,按LRU淘汰 while (this.cache.size > targetSize && this.accessOrder.size > 0) { const oldestKey = this.accessOrder.keys().next().value; this.cache.delete(oldestKey); this.accessOrder.delete(oldestKey); removedCount++; } if (removedCount > 0) { this.stats.evictions += removedCount; console.log(`[${this.name}] Aggressive cleanup: ${removedCount} items evicted`); } } /** * 检查是否需要淘汰并执行LRU淘汰 */ _evictIfNeeded() { while (this.cache.size >= this.maxSize && this.accessOrder.size > 0) { const oldestKey = this.accessOrder.keys().next().value; this.cache.delete(oldestKey); this.accessOrder.delete(oldestKey); this.stats.evictions++; } } /** * 估算内存使用量(简单估算) * @returns {string} */ _estimateMemoryUsage() { const avgKeySize = 50; // 平均键长度 const avgValueSize = 1024; // 平均值大小(1KB) const estimatedBytes = this.cache.size * (avgKeySize + avgValueSize); if (estimatedBytes < 1024) return `${estimatedBytes}B`; if (estimatedBytes < 1024 * 1024) return `${(estimatedBytes / 1024).toFixed(1)}KB`; return `${(estimatedBytes / (1024 * 1024)).toFixed(1)}MB`; } } /** * 验证身份认证 * @param {Object} request - Fastify请求对象 * @param {Object} reply - Fastify响应对象 * @returns {boolean} 验证是否通过 */ export function verifyAuth(request, reply) { const requiredAuth = ENV.get('PROXY_AUTH', 'drpys'); const providedAuth = request.query.auth; if (!providedAuth || providedAuth !== requiredAuth) { reply.status(401).send({ error: 'Unauthorized', message: 'Missing or invalid auth parameter', code: 401 }); return false; } return true; } /** * 解码参数 - 支持 base64 解码 * @param {string} param - 需要解码的参数 * @param {boolean} isJson - 是否为 JSON 格式 * @returns {string|Object} 解码后的参数 */ export function decodeParam(param, isJson = false) { if (!param) return isJson ? {} : ''; let decoded = param; try { // 首先尝试 URL 解码 decoded = decodeURIComponent(param); } catch (e) { // URL 解码失败,使用原始参数 decoded = param; } // 对于 URL 参数,如果不是 http 开头,尝试 base64 解码 if (!isJson && !decoded.startsWith('http://') && !decoded.startsWith('https://')) { try { const base64Decoded = Buffer.from(decoded, 'base64').toString('utf8'); if (base64Decoded.startsWith('http://') || base64Decoded.startsWith('https://')) { decoded = base64Decoded; } } catch (e) { // base64 解码失败,保持原值 } } // 对于 headers 参数,如果不是 JSON 格式,尝试 base64 解码 if (isJson && !decoded.startsWith('{') && !decoded.endsWith('}')) { try { const base64Decoded = Buffer.from(decoded, 'base64').toString('utf8'); if (base64Decoded.startsWith('{') && base64Decoded.endsWith('}')) { decoded = base64Decoded; } } catch (e) { // base64 解码失败,保持原值 } } // 如果是 JSON 格式,尝试解析 if (isJson) { try { return JSON.parse(decoded); } catch (e) { console.warn('Failed to parse headers as JSON:', decoded); return {}; } } return decoded; } /** * 获取默认请求头 * @param {Object} request - Fastify 请求对象 * @returns {Object} 默认请求头 */ export function getDefaultHeaders(request) { const defaultHeaders = {}; // 复制一些重要的请求头 const headersToForward = [ 'user-agent', 'accept', 'accept-language', 'accept-encoding', 'referer', 'origin' ]; headersToForward.forEach(header => { if (request.headers[header]) { defaultHeaders[header] = request.headers[header]; } }); // 如果没有 user-agent,设置默认值 if (!defaultHeaders['user-agent']) { defaultHeaders['user-agent'] = PROXY_CONSTANTS.DEFAULT_USER_AGENT; } return defaultHeaders; } /** * 发起远程请求 * @param {string} url - 远程文件 URL * @param {Object} headers - 请求头 * @param {string} method - 请求方法 * @param {string} range - Range 头 * @param {number} timeout - 超时时间(毫秒) * @returns {Promise} 请求结果 */ export function makeRemoteRequest(url, headers, method = 'GET', range = null, timeout = PROXY_CONSTANTS.REQUEST_TIMEOUT) { return new Promise((resolve, reject) => { let isResolved = false; try { const urlObj = new URL(url); const isHttps = urlObj.protocol === 'https:'; const httpModule = isHttps ? https : http; const requestHeaders = { ...headers }; if (range) { requestHeaders['range'] = range; } const options = { hostname: urlObj.hostname, port: urlObj.port || (isHttps ? 443 : 80), path: urlObj.pathname + urlObj.search, method: method, headers: requestHeaders, timeout: timeout }; const req = httpModule.request(options, (res) => { if (isResolved) return; isResolved = true; resolve({ statusCode: res.statusCode, headers: res.headers, stream: res }); }); req.on('error', (error) => { if (isResolved) return; isResolved = true; reject(new Error(`Request failed: ${error.message}`)); }); req.on('timeout', () => { if (isResolved) return; isResolved = true; req.destroy(); reject(new Error('Request timeout')); }); req.setTimeout(timeout, () => { if (isResolved) return; isResolved = true; req.destroy(); reject(new Error('Request timeout')); }); req.end(); } catch (error) { if (isResolved) return; isResolved = true; reject(new Error(`Invalid URL or request setup: ${error.message}`)); } }); } /** * 发起远程HEAD请求检测内容类型 * @param {string} url - 远程文件 URL * @param {Object} headers - 请求头 * @returns {Promise} 请求结果 */ export function makeHeadRequest(url, headers) { return makeRemoteRequest(url, headers, 'HEAD', null, PROXY_CONSTANTS.HEAD_REQUEST_TIMEOUT); } /** * 获取远程文件内容(文本) * @param {string} url - 远程文件 URL * @param {Object} headers - 请求头 * @returns {Promise} 文件内容 */ export function getRemoteContent(url, headers) { return new Promise(async (resolve, reject) => { let isResolved = false; let timeoutId; try { // 设置总体超时 timeoutId = setTimeout(() => { if (isResolved) return; isResolved = true; reject(new Error('Content fetch timeout')); }, PROXY_CONSTANTS.CONTENT_FETCH_TIMEOUT); const response = await makeRemoteRequest(url, headers, 'GET'); if (response.statusCode >= 400) { if (isResolved) return; isResolved = true; clearTimeout(timeoutId); reject(new Error(`Remote server error: ${response.statusCode}`)); return; } let content = ''; let contentLength = 0; response.stream.on('data', chunk => { if (isResolved) return; contentLength += chunk.length; if (contentLength > PROXY_CONSTANTS.MAX_CONTENT_LENGTH) { isResolved = true; clearTimeout(timeoutId); reject(new Error('Content too large')); return; } content += chunk.toString('utf8'); }); response.stream.on('end', () => { if (isResolved) return; isResolved = true; clearTimeout(timeoutId); resolve(content); }); response.stream.on('error', (error) => { if (isResolved) return; isResolved = true; clearTimeout(timeoutId); reject(error); }); } catch (error) { if (isResolved) return; isResolved = true; clearTimeout(timeoutId); reject(error); } }); } /** * 获取请求的基础 URL(协议 + 主机 + 端口) * @param {Object} request - Fastify 请求对象 * @returns {string} 基础 URL */ export function getProxyBaseUrl(request) { const protocol = request.headers['x-forwarded-proto'] || (request.connection.encrypted ? 'https' : 'http'); const host = request.headers['x-forwarded-host'] || request.headers.host || 'localhost:3001'; return `${protocol}://${host}`; } /** * 设置通用的CORS响应头 * @param {Object} reply - Fastify响应对象 */ export function setCorsHeaders(reply) { reply.header('Access-Control-Allow-Origin', '*'); reply.header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS'); reply.header('Access-Control-Allow-Headers', 'Range, Content-Type'); } /** * 转发重要的响应头 * @param {Object} reply - Fastify响应对象 * @param {Object} remoteHeaders - 远程响应头 */ export function forwardResponseHeaders(reply, remoteHeaders) { const headersToForward = [ 'content-type', 'content-length', 'content-range', 'accept-ranges', 'last-modified', 'etag', 'cache-control', 'expires' ]; headersToForward.forEach(header => { if (remoteHeaders[header]) { reply.header(header, remoteHeaders[header]); } }); } /** * 验证URL格式 * @param {string} url - 要验证的URL * @returns {boolean} URL是否有效 */ export function isValidUrl(url) { return url && (url.startsWith('http://') || url.startsWith('https://')); } /** * 检查是否为内网IP地址 * @param {string} url - 要检查的URL * @returns {boolean} 是否为内网IP */ export function isInternalIp(url) { try { const urlObj = new URL(url); const hostname = urlObj.hostname; // 检查是否为IP地址 const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; if (!ipRegex.test(hostname)) { return false; // 不是IP地址,可能是域名 } const parts = hostname.split('.').map(Number); // 检查内网IP范围 // 10.0.0.0/8 if (parts[0] === 10) return true; // 172.16.0.0/12 if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 192.168.0.0/16 if (parts[0] === 192 && parts[1] === 168) return true; // 127.0.0.0/8 (localhost) if (parts[0] === 127) return true; return false; } catch (error) { return false; } } /** * 创建缓存管理器工厂函数 */ export const CacheManagerFactory = { /** * 创建标准请求缓存管理器 * @param {string} name - 缓存名称 * @returns {SmartCacheManager} */ createRequestCache(name = 'RequestCache') { return new SmartCacheManager({ name, maxSize: PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_MAX_SIZE, defaultTTL: PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_TTL, cleanupInterval: PROXY_CONSTANTS.CACHE_MANAGER.CLEANUP_INTERVAL }); }, /** * 创建M3U8缓存管理器(短TTL) * @param {string} name - 缓存名称 * @returns {SmartCacheManager} */ createM3U8Cache(name = 'M3U8Cache') { return new SmartCacheManager({ name, maxSize: 500, // M3U8缓存较小 defaultTTL: PROXY_CONSTANTS.CACHE_MANAGER.M3U8_TTL, cleanupInterval: 30 * 1000 // 30秒清理一次 }); }, /** * 创建文件缓存管理器 * @param {string} name - 缓存名称 * @returns {SmartCacheManager} */ createFileCache(name = 'FileCache') { return new SmartCacheManager({ name, maxSize: 2000, // 文件缓存可以更大 defaultTTL: PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_TTL, cleanupInterval: PROXY_CONSTANTS.CACHE_MANAGER.CLEANUP_INTERVAL }); }, /** * 创建客户端连接缓存管理器 * @param {string} name - 缓存名称 * @returns {SmartCacheManager} */ createClientCache(name = 'ClientCache') { return new SmartCacheManager({ name, maxSize: 100, // 客户端连接数量较少 defaultTTL: PROXY_CONSTANTS.CACHE_MANAGER.DEFAULT_TTL, cleanupInterval: PROXY_CONSTANTS.CACHE_MANAGER.CLEANUP_INTERVAL }); }, /** * 创建自定义缓存管理器 * @param {Object} options - 配置选项 * @returns {SmartCacheManager} */ createCustomCache(options) { return new SmartCacheManager(options); } }; /** * 创建标准的健康检查响应 * @param {string} serviceName - 服务名称 * @param {Map|SmartCacheManager} requestCache - 请求缓存 * @param {Map|SmartCacheManager} additionalCache - 额外缓存(可选) * @returns {Object} 健康检查响应 */ export function createHealthResponse(serviceName, requestCache, additionalCache = null) { const response = { status: 'ok', service: serviceName, timestamp: new Date().toISOString(), cache: {} }; // 支持新的SmartCacheManager和旧的Map if (requestCache) { if (typeof requestCache.getStats === 'function') { response.cache.requests = { size: requestCache.size, stats: requestCache.getStats() }; } else { response.cache.requests = requestCache.size; } } if (additionalCache) { if (typeof additionalCache.getStats === 'function') { response.cache.additional = { size: additionalCache.size, stats: additionalCache.getStats() }; } else { response.cache.additional = additionalCache.size; } } return response; } /** * 创建标准的状态响应 * @param {string} serviceName - 服务名称 * @param {string} version - 版本号 * @param {Array} features - 功能列表 * @param {Array} endpoints - 端点列表 * @param {Map|SmartCacheManager} requestCache - 请求缓存 * @param {Map|SmartCacheManager} additionalCache - 额外缓存(可选) * @param {Object} additionalInfo - 额外信息(可选) * @returns {Object} 状态响应 */ export function createStatusResponse(serviceName, version, features, endpoints, requestCache, additionalCache = null, additionalInfo = {}) { const response = { service: serviceName, version: version, status: 'running', cache: { timeout: PROXY_CONSTANTS.CACHE_TIMEOUT }, features: features, endpoints: endpoints, auth: { required: true, parameter: 'auth', description: 'Authentication code required for protected endpoints' }, ...additionalInfo }; // 支持新的SmartCacheManager和旧的Map if (requestCache) { if (typeof requestCache.getStats === 'function') { response.cache.requests = { size: requestCache.size, stats: requestCache.getStats() }; } else { response.cache.requests = requestCache.size; } } if (additionalCache) { if (typeof additionalCache.getStats === 'function') { response.cache.additional = { size: additionalCache.size, stats: additionalCache.getStats() }; response.cache.additionalTimeout = PROXY_CONSTANTS.M3U8_CACHE_TIMEOUT; } else { response.cache.additional = additionalCache.size; response.cache.additionalTimeout = PROXY_CONSTANTS.M3U8_CACHE_TIMEOUT; } } return response; }