122 lines
3.3 KiB
JavaScript
Executable File
122 lines
3.3 KiB
JavaScript
Executable File
/**
|
||
* 会话 Cookie 持久化:按 sessionKey 保存 cf_clearance 等,降低重复过盾概率
|
||
*/
|
||
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');
|
||
|
||
/**
|
||
* 确保 sessions 目录存在
|
||
*/
|
||
function ensureSessionsDir() {
|
||
fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o777 });
|
||
}
|
||
|
||
/**
|
||
* sessionKey 转安全文件名
|
||
* @param {string} sessionKey
|
||
* @returns {string}
|
||
*/
|
||
function keyToFilename(sessionKey) {
|
||
const hash = crypto.createHash('md5').update(sessionKey).digest('hex');
|
||
return `${hash}.json`;
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} StoredSession
|
||
* @property {string} sessionKey
|
||
* @property {number} savedAt
|
||
* @property {number} expiresAt
|
||
* @property {Array<{name:string,value:string,domain?:string,path?:string}>} cookies
|
||
*/
|
||
|
||
/**
|
||
* 加载已保存的会话 cookies
|
||
* @param {string} sessionKey
|
||
* @returns {StoredSession|null}
|
||
*/
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存会话 cookies(优先保留 CF 相关 cookie)
|
||
* @param {string} sessionKey
|
||
* @param {Array<{name:string,value:string,domain?:string,path?:string}>} cookies
|
||
* @param {number} [ttlMs]
|
||
*/
|
||
function saveSession(sessionKey, cookies, ttlMs = SESSION_TTL_MS) {
|
||
if (!sessionKey || !Array.isArray(cookies) || cookies.length === 0) return;
|
||
ensureSessionsDir();
|
||
|
||
const cfRelated = cookies.filter((c) =>
|
||
['cf_clearance', '__cf_bm', 'cf_chl_2'].includes(c.name)
|
||
|| c.name.startsWith('__cf')
|
||
);
|
||
|
||
const toSave = cfRelated.length > 0 ? cfRelated : cookies;
|
||
const now = Date.now();
|
||
const payload = {
|
||
sessionKey,
|
||
savedAt: now,
|
||
expiresAt: now + ttlMs,
|
||
cookies: toSave.map((c) => ({
|
||
name: c.name,
|
||
value: c.value,
|
||
domain: c.domain,
|
||
path: c.path || '/',
|
||
})),
|
||
};
|
||
|
||
const filePath = path.join(SESSIONS_DIR, keyToFilename(sessionKey));
|
||
fs.writeFileSync(filePath, JSON.stringify(payload, null, 0), { mode: 0o666 });
|
||
}
|
||
|
||
/**
|
||
* 将会话 cookies 转为 puppeteer setCookie 格式
|
||
* @param {StoredSession|null} session
|
||
* @param {string} pageUrl
|
||
* @returns {Array<{name:string,value:string,domain?:string,path?:string,url?:string}>}
|
||
*/
|
||
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;
|
||
});
|
||
}
|
||
|
||
module.exports = {
|
||
loadSession,
|
||
saveSession,
|
||
sessionCookiesForPage,
|
||
};
|