/** * 会话 Cookie 持久化:按 sessionKey 保存完整 cookie jar,降低重复过盾概率 */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { RUNTIME_DIR, SESSION_TTL_MS } = require('./constants'); const SESSIONS_DIR = path.join(RUNTIME_DIR, 'sessions'); function ensureSessionsDir() { fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o777 }); } function keyToFilename(sessionKey) { const hash = crypto.createHash('md5').update(sessionKey).digest('hex'); return `${hash}.json`; } /** * @param {{name:string,domain?:string,path?:string}} cookie * @returns {string} */ function cookieIdentity(cookie) { return `${cookie.name}|${cookie.domain || ''}|${cookie.path || '/'}`; } function mergeCookies(requestCookies, sessionCookies) { const map = new Map(); for (const cookie of sessionCookies || []) { if (cookie && cookie.name) map.set(cookieIdentity(cookie), cookie); } for (const cookie of requestCookies || []) { if (cookie && cookie.name) map.set(cookieIdentity(cookie), cookie); } return Array.from(map.values()); } function isSessionLikelyValid(session) { if (!session || !Array.isArray(session.cookies)) return false; if (session.expiresAt && Date.now() > session.expiresAt) return false; return session.cookies.some((c) => c.name === 'cf_clearance' && c.value); } function hasCfClearance(session) { if (!session || !Array.isArray(session.cookies)) return false; return session.cookies.some((c) => c.name === 'cf_clearance' && c.value); } function loadSession(sessionKey) { if (!sessionKey) return null; ensureSessionsDir(); const filePath = path.join(SESSIONS_DIR, keyToFilename(sessionKey)); if (!fs.existsSync(filePath)) return null; try { const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (!raw || !Array.isArray(raw.cookies)) return null; if (raw.expiresAt && Date.now() > raw.expiresAt) { fs.unlinkSync(filePath); return null; } return raw; } catch (_) { return null; } } function touchSession(sessionKey, ttlMs = SESSION_TTL_MS) { const session = loadSession(sessionKey); if (!session) return; const now = Date.now(); session.savedAt = now; session.expiresAt = now + ttlMs; fs.writeFileSync(path.join(SESSIONS_DIR, keyToFilename(sessionKey)), JSON.stringify(session, null, 0), { mode: 0o666 }); } function saveSession(sessionKey, cookies, ttlMs = SESSION_TTL_MS, meta = {}) { if (!sessionKey || !Array.isArray(cookies) || cookies.length === 0) return; ensureSessionsDir(); const now = Date.now(); const payload = { sessionKey, savedAt: now, lastSuccessAt: now, expiresAt: now + ttlMs, lastHost: meta.lastHost || '', savedProfile: meta.savedProfile || '', cookies: cookies.map((c) => ({ name: c.name, value: c.value, domain: c.domain, path: c.path || '/', })), }; fs.writeFileSync(path.join(SESSIONS_DIR, keyToFilename(sessionKey)), JSON.stringify(payload, null, 0), { mode: 0o666 }); } function sessionCookiesForPage(session, pageUrl) { if (!session || !Array.isArray(session.cookies)) return []; let hostname = ''; try { hostname = new URL(pageUrl).hostname; } catch (_) { return session.cookies; } return session.cookies.map((c) => { const cookie = { ...c }; if (!cookie.domain && hostname) cookie.url = `https://${hostname}/`; return cookie; }); } function getSessionDebugInfo(sessionKey) { if (!sessionKey) return null; const filePath = path.join(SESSIONS_DIR, keyToFilename(sessionKey)); const session = loadSession(sessionKey); if (!session) { return { sessionKey, exists: false, filePath }; } let mtime = null; try { mtime = fs.statSync(filePath).mtime.toISOString(); } catch (_) { /* ignore */ } return { sessionKey, exists: true, filePath, mtime, savedAt: session.savedAt, expiresAt: session.expiresAt, lastSuccessAt: session.lastSuccessAt, lastHost: session.lastHost, savedProfile: session.savedProfile, cookieCount: session.cookies.length, hasCfClearance: hasCfClearance(session), likelyValid: isSessionLikelyValid(session), }; } function getSessionStats() { ensureSessionsDir(); const count = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json')).length; return { sessionsDir: SESSIONS_DIR, count }; } module.exports = { loadSession, saveSession, touchSession, sessionCookiesForPage, mergeCookies, isSessionLikelyValid, hasCfClearance, getSessionDebugInfo, getSessionStats, };