2026-06-20 04:47:34 +08:00
|
|
|
/**
|
2026-06-20 15:49:40 +08:00
|
|
|
* 会话 Cookie 持久化:按 sessionKey 保存完整 cookie jar,降低重复过盾概率
|
2026-06-20 04:47:34 +08:00
|
|
|
*/
|
|
|
|
|
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`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-06-20 15:49:40 +08:00
|
|
|
* @param {{name:string,domain?:string,path?:string}} cookie
|
|
|
|
|
* @returns {string}
|
2026-06-20 04:47:34 +08:00
|
|
|
*/
|
2026-06-20 15:49:40 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-06-20 04:47:34 +08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 15:49:40 +08:00
|
|
|
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 = {}) {
|
2026-06-20 04:47:34 +08:00
|
|
|
if (!sessionKey || !Array.isArray(cookies) || cookies.length === 0) return;
|
|
|
|
|
ensureSessionsDir();
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const payload = {
|
|
|
|
|
sessionKey,
|
|
|
|
|
savedAt: now,
|
2026-06-20 15:49:40 +08:00
|
|
|
lastSuccessAt: now,
|
2026-06-20 04:47:34 +08:00
|
|
|
expiresAt: now + ttlMs,
|
2026-06-20 15:49:40 +08:00
|
|
|
lastHost: meta.lastHost || '',
|
|
|
|
|
savedProfile: meta.savedProfile || '',
|
|
|
|
|
cookies: cookies.map((c) => ({
|
2026-06-20 04:47:34 +08:00
|
|
|
name: c.name,
|
|
|
|
|
value: c.value,
|
|
|
|
|
domain: c.domain,
|
|
|
|
|
path: c.path || '/',
|
|
|
|
|
})),
|
|
|
|
|
};
|
2026-06-20 15:49:40 +08:00
|
|
|
fs.writeFileSync(path.join(SESSIONS_DIR, keyToFilename(sessionKey)), JSON.stringify(payload, null, 0), { mode: 0o666 });
|
2026-06-20 04:47:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 };
|
2026-06-20 15:49:40 +08:00
|
|
|
if (!cookie.domain && hostname) cookie.url = `https://${hostname}/`;
|
2026-06-20 04:47:34 +08:00
|
|
|
return cookie;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 15:49:40 +08:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 04:47:34 +08:00
|
|
|
module.exports = {
|
|
|
|
|
loadSession,
|
|
|
|
|
saveSession,
|
2026-06-20 15:49:40 +08:00
|
|
|
touchSession,
|
2026-06-20 04:47:34 +08:00
|
|
|
sessionCookiesForPage,
|
2026-06-20 15:49:40 +08:00
|
|
|
mergeCookies,
|
|
|
|
|
isSessionLikelyValid,
|
|
|
|
|
hasCfClearance,
|
|
|
|
|
getSessionDebugInfo,
|
|
|
|
|
getSessionStats,
|
2026-06-20 04:47:34 +08:00
|
|
|
};
|