Files
daily-checkin-panel/test/cookie-tools.test.mjs
hani 987793c667 借鉴 OpenList 凭据续期:扩展凭据包加 pageUrl;服务端 Set-Cookie 静默回写(cookieRefresh)
- 扩展 popup.js:payload 增加 pageUrl(采集时的完整页面地址),版本 2.17 → 2.18
- 面板:粘贴解析透传 pageUrl;一键发送 handoff 存 pageUrl;autoApplyCookies 支持站点声明 page_url 字段自动填入
- runner:新增 applyCookieRefresh —— 站点签到成功后经 res.cookieRefresh 带回的新 Cookie 就地合并、加密存回 D1(标准 cookie 字段 / headers JSON / 多步 steps 三种存法;模板 {{cookie}} 跳过;无变化不写库)
- http.js runHttpSteps:自动收集各步骤 Set-Cookie(自定义 HTTP 与社区站点生效);community.js、单步 run 透传 cookieRefresh
- v2ex.js:收集签到响应的 Set-Cookie 并返回 cookieRefresh
- web.js cookiesFrom:永不抛错(读不到返回 '')
- Akile 懒续期复查:已有预刷新 + 失败刷新重试 + D1 回写,无缺口,不动
- 文档:详细手册补「服务端直签续命」与「字段约定(cookie/user_agent/page_url/site_url)+ 凭据包格式」;CHANGELOG 记入未发布
- 测试:新增 test/cookie-refresh.test.mjs(11 组);cookie-tools.test.mjs 补 pageUrl 解析(2 组)
2026-09-29 23:43:19 +08:00

261 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// cookie-tools 测试:node test/cookie-tools.test.mjs
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import assert from 'node:assert/strict';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
eval(readFileSync(join(root, 'public', 'curl-import.js'), 'utf8'));
eval(readFileSync(join(root, 'public', 'cookie-tools.js'), 'utf8'));
const { parseCookieHeader, extractCookieFromHeaders, parseCookieEditorJson, parseNetscapeCookies, extractCookieFromCurl } = globalThis;
let n = 0;
const t = (name, fn) => { fn(); n++; console.log('ok -', name); };
t('原始 Cookie 字符串规范化', () => {
assert.equal(parseCookieHeader('a=1;b=2;; c=3 '), 'a=1; b=2; c=3');
assert.equal(parseCookieHeader('Cookie: x=y'), 'x=y');
});
t('原始字符串为空报错', () => {
assert.throws(() => parseCookieHeader(' '), /内容为空/);
});
t('从请求头文本提取', () => {
const txt = 'GET / HTTP/1.1\nHost: www.nodeseek.com\nCookie: a=1; b=2\nUser-Agent: x';
// 同时带回 UA,便于吾爱等需要 UA 与 Cookie 配对的站点直接填入
assert.deepEqual(extractCookieFromHeaders(txt), { cookie: 'a=1; b=2', userAgent: 'x' });
});
t('请求头无 Cookie 报错', () => {
assert.throws(() => extractCookieFromHeaders('Host: a.com\nX: 1'), /没有找到 Cookie/);
});
t('Cookie-Editor JSON 导入', () => {
const json = JSON.stringify([
{ name: 'a', value: '1', domain: '.nodeseek.com' },
{ name: 'b', value: 'hello world' },
{ name: '', value: 'skip' },
]);
assert.equal(parseCookieEditorJson(json), 'a=1; b=hello world');
});
t('JSON 格式错误报错', () => {
assert.throws(() => parseCookieEditorJson('not json'), /JSON 解析失败/);
assert.throws(() => parseCookieEditorJson('{"a":1}'), /JSON 数组/);
});
t('Netscape cookie.txt 导入', () => {
const txt = [
'# Netscape HTTP Cookie File',
'#HttpOnly_.nodeseek.com\tTRUE\t/\tFALSE\t1234567890\tsess\tabc123',
'.nodeseek.com\tTRUE\t/\tFALSE\t1234567890\tuid\t42',
'',
].join('\n');
assert.equal(parseNetscapeCookies(txt), 'sess=abc123; uid=42');
});
t('cookie.txt 无效报错', () => {
assert.throws(() => parseNetscapeCookies('# only comments\n'), /没有解析到/);
});
t('从 cURL 提取 Cookie(-H)', () => {
const r = extractCookieFromCurl(`curl 'https://www.nodeseek.com/api/attendance?random=true' -H 'Cookie: a=1;b=2' -H 'User-Agent: MyUA/1.0'`);
assert.equal(r.cookie, 'a=1; b=2');
assert.equal(r.userAgent, 'MyUA/1.0');
});
t('从 cURL 提取 Cookie(-b)', () => {
const r = extractCookieFromCurl(`curl -b 'x=9' 'https://example.com/'`);
assert.equal(r.cookie, 'x=9');
assert.equal(r.userAgent, '');
});
t('cURL 无 Cookie 报错', () => {
assert.throws(() => extractCookieFromCurl(`curl 'https://example.com/'`), /没有 Cookie/);
});
// ---------- Cookie 逐段中文说明(面板「Cookie 解析器」的核心字典)----------
// 目的:用户粘一大串时能看懂哪段是登录必需、哪段是网站安全校验、哪些只是统计。
t('cookie 说明:登录必需 / 安全校验 / 统计 三类能分清', () => {
assert.equal(describeCookieName('PHPSESSID').kind, 'login');
assert.match(describeCookieName('PHPSESSID').desc, /PHP 会话/);
assert.equal(describeCookieName('wordpress_logged_in_abc').kind, 'login');
assert.match(describeCookieName('wordpress_logged_in_abc').desc, /WordPress/);
// 吾爱破解的 WAF 校验 Cookie 必须单独归为「安全校验」——它失效就等于 403
assert.equal(describeCookieName('wzws_cid').kind, 'waf');
assert.match(describeCookieName('wzws_cid').desc, /网宿/);
assert.equal(describeCookieName('cf_clearance').kind, 'waf');
assert.equal(describeCookieName('_ga').kind, 'stat');
assert.equal(describeCookieName('Hm_lvt_1234').kind, 'stat');
});
t('cookie 说明:不认识的名字归为「站点自定义」,不能瞎猜', () => {
const d = describeCookieName('some_random_bbs_key');
assert.equal(d.kind, 'other');
assert.match(d.desc, /站点自定义/);
});
t('cookie 值说明:只讲形态、不展开值,长度和类型都要报出来', () => {
const jwt = describeCookieValue('eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEyMzQ1Njc4fQ.abcdefghijklmn');
assert.match(jwt, /长度 \d+/);
assert.match(jwt, /JWT/);
assert.match(describeCookieValue(String(Math.floor(Date.now() / 1000))), /Unix 时间戳/);
assert.match(describeCookieValue(''), /空值/);
// 值不能整段吐出来:只留头尾做比对用
const long = 'x'.repeat(40);
const shown = describeCookieValue(long);
assert.ok(!shown.includes(long), '不应把完整值原样展示');
});
// ---------- parsePasteText / explainCookieText:面板「Cookie 解析器」的底座 ----------
// 用户会粘进来的东西很杂(扩展 JSON / 请求头 / cURL / cookie.txt / 裸串),都要认。
t('识别:裸 Cookie 串', () => {
const r = parsePasteText('a=1; b=2');
assert.equal(r.format, '纯 Cookie 字符串');
assert.equal(r.cookie, 'a=1; b=2');
});
t('识别:F12 请求头(Cookie + UA 一起拿)', () => {
const r = parsePasteText('Accept: */*\nCookie: PHPSESSID=abc; token=xyz\nUser-Agent: MyUA/9.9');
assert.equal(r.format, '浏览器复制的请求头');
assert.equal(r.cookie, 'PHPSESSID=abc; token=xyz');
assert.equal(r.userAgent, 'MyUA/9.9');
});
t('识别:扩展「一键复制全部信息」的 JSON', () => {
const payload = JSON.stringify({
domain: 'www.52pojie.cn', cookies: 'wzws_cid=abc; cdb_sid=1', userAgent: 'UA-from-ext',
cookieList: [{ name: 'wzws_cid', value: 'abc', httpOnly: true }, { name: 'cdb_sid', value: '1' }],
localStorage: { a: '1' },
});
const r = parsePasteText(payload);
assert.equal(r.format, '扩展「一键复制全部信息」');
assert.equal(r.domain, 'www.52pojie.cn');
assert.equal(r.userAgent, 'UA-from-ext');
assert.equal(r.cookieList.length, 2);
});
t('识别:扩展 JSON 里的 pageUrl(采集时的完整页面地址)被带出来', () => {
const payload = JSON.stringify({
domain: 'www.v2ex.com', pageUrl: 'https://www.v2ex.com/mission/daily',
cookies: 'A2=abc', userAgent: 'UA/1',
});
const r = parsePasteText(payload);
assert.equal(r.pageUrl, 'https://www.v2ex.com/mission/daily');
// 旧版扩展没这个字段:解析不能炸,缺省为空
const r2 = parsePasteText(JSON.stringify({ domain: 'a.com', cookies: 'x=1' }));
assert.equal(r2.pageUrl, '');
});
t('识别:不认识的内容要报错,不能默默当成 Cookie', () => {
assert.throws(() => parsePasteText('这是一段普通文字,没有任何等号'), /没看出这是 Cookie/);
assert.throws(() => parsePasteText(''), /请先在目标网站/);
});
t('explainCookieText:每一段都给出类型 + 中文说明 + 值形态', () => {
const raw = 'Cookie: PHPSESSID=abc123; wordpress_logged_in_a=other-user%7C1730000000%7Cabcdef; wzws_cid=0123456789abcdef0123456789abcdef; _ga=GA1.2.99';
const info = explainCookieText(raw);
assert.equal(info.parts.length, 4);
const byName = Object.fromEntries(info.parts.map((p) => [p.name, p]));
assert.equal(byName.PHPSESSID.kind, 'login');
assert.match(byName.PHPSESSID.desc, /PHP 会话/);
assert.equal(byName.wordpress_logged_in_a.kind, 'login');
// 吾爱破解的 WAF cookie 必须被单独标出来,用户才知道它不能丢
assert.equal(byName.wzws_cid.kind, 'waf');
assert.equal(byName._ga.kind, 'stat');
// 值形态:长度 + 特征,不把整段值吐出来
assert.match(byName.PHPSESSID.detail, /长度 6/);
assert.ok(!byName.PHPSESSID.detail.includes('abc123'), '不应原样展示值');
// 小结要把“几段登录必需 / 几段安全校验”说人话
assert.match(info.summary, /共 4 段/);
assert.match(info.summary, /登录必需 2 段/);
assert.match(info.summary, /安全校验 1 段/);
assert.match(info.summary, /纯统计 1 段/);
});
// ---------------------------------------------------------------------------
// 粘贴内容的体检(checkPastedCreds):保存前把能查的都查一遍
// ---------------------------------------------------------------------------
t('体检:一份正常的扩展 JSON → 通过,并说清每一段是什么', () => {
const info = parsePasteText(JSON.stringify({
domain: 'dj.hutue.cn',
cookies: 'wordpress_logged_in_a=other-user%7C1; _ga=GA1.2.3',
userAgent: 'UA',
}));
const r = globalThis.checkPastedCreds(info);
assert.equal(r.level, 'ok');
assert.ok(r.items.some((i) => /认出了格式/.test(i.title)));
assert.ok(r.items.some((i) => /会话|登录必需/.test(i.detail || '')));
assert.ok(r.items.some((i) => /来自网站:dj\.hutue\.cn/.test(i.title)));
});
t('体检:只有统计类 Cookie → 提醒可能没登录态', () => {
const r = globalThis.checkPastedCreds(parsePasteText('_ga=GA1.2.3; theme=dark'));
assert.equal(r.level, 'warn');
assert.ok(r.items.some((i) => i.level === 'warn' && /没看出「登录必需」/.test(i.title)));
// 裸 Cookie 串里没有域名 —— 必须提醒得手动选站点
assert.ok(r.items.some((i) => i.level === 'warn' && /没带域名/.test(i.title)));
assert.ok(!r.items.some((i) => i.level === 'bad'));
});
t('体检:空 Cookie → 硬错误,不许保存', () => {
const r = globalThis.checkPastedCreds({ cookie: '', format: '扩展「一键复制全部信息」' });
assert.equal(r.level, 'bad');
assert.match(r.items[0].title, /没有读到 Cookie/);
assert.match(r.items[0].detail, /一键复制全部信息/);
});
t('体检:空值 Cookie 会被点名(最容易“看着成功其实没抓到”)', () => {
const r = globalThis.checkPastedCreds(parsePasteText('PHPSESSID=; cdb_sid='));
assert.equal(r.level, 'warn');
const empty = r.items.find((i) => /空值/.test(i.title));
assert.ok(empty);
assert.match(empty.detail, /PHPSESSID/);
});
t('粘贴没复制全的 JSON:要说“像没复制全”,而不是只报 JSON 错了', () => {
assert.throws(
() => parsePasteText('{"domain":"dj.hutue.cn","cookies":"a=1"'),
/没复制全/);
assert.throws(() => parsePasteText('{"a":1}'), /没有 Cookie/);
});
// 线上真实案例(2026-09-29):hutue.cn 的账号里混着 dj.hutue.cn 的登录会话,
// 用户一直以为「我更新了 Cookie」,而属于 hutue.cn 的那段早就过期了。
t('体检:登录会话已过期 → 提醒(不再硬拦),并给出两条出路', () => {
const past = Math.floor(Date.now() / 1000) - 3600;
const r = globalThis.checkPastedCreds({
cookie: `wordpress_logged_in_ec35f1949aa62d7b02e78d74b17cb6b5=demo-user%7C${past}%7Ct%7Ch`,
format: '扩展「一键复制全部信息」', domain: 'hutue.cn',
});
assert.equal(r.level, 'warn', '不再判 bad:填了账号密码的站点能自动重新登录,不该把保存按钮封死');
const it = r.items.find((i) => /登录会话已经过期/.test(i.title));
assert.ok(it, JSON.stringify(r.items.map((i) => i.title)));
assert.match(it.detail, /账号密码/);
assert.match(it.detail, /重新登录/);
});
t('体检:一份 Cookie 里混了两个域名的登录会话 → 点名说出来', () => {
const future = Math.floor(Date.now() / 1000) + 86400;
const r = globalThis.checkPastedCreds({
cookie: `wordpress_logged_in_ec35f1949aa62d7b02e78d74b17cb6b5=u1%7C${future}%7Ct%7Ch; `
+ `wordpress_logged_in_ca7674586a167c665930997282100f84=u2%7C${future}%7Ct%7Ch`,
cookieList: [
{ name: 'wordpress_logged_in_ec35f1949aa62d7b02e78d74b17cb6b5', value: 'x', domain: '.hutue.cn' },
{ name: 'wordpress_logged_in_ca7674586a167c665930997282100f84', value: 'x', domain: 'dj.hutue.cn' },
],
format: '扩展「一键复制全部信息」', domain: 'hutue.cn',
});
assert.equal(r.level, 'warn');
const mixed = r.items.find((i) => /混了两个域名/.test(i.title));
assert.ok(mixed, JSON.stringify(r.items.map((i) => i.title)));
assert.match(mixed.detail, /另一个站/);
// 会话那段要说清是哪个域名来的,用户才能自己去对
const sess = r.items.find((i) => /登录会话有效/.test(i.title));
assert.match(sess.detail, /来自 \.hutue\.cn/);
assert.match(sess.detail, /来自 dj\.hutue\.cn/);
});
console.log(`\n${n} 组通过`);