110 lines
3.3 KiB
JavaScript
110 lines
3.3 KiB
JavaScript
// 人机验证全链路自测(Node 侧算 PoW,与服务端 webcrypto 同算法)
|
||||
|
|
import crypto from 'node:crypto';
|
|||
|
|
|
|||
|
|
const BASE = 'https://api.200181.xyz/api/v2/human';
|
|||
|
|
const POW_DIFFICULTY = 4;
|
|||
|
|
|
|||
|
|
function sha256Hex(s) {
|
|||
|
|
return crypto.createHash('sha256').update(s).digest('hex');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function solve(challenge, difficulty) {
|
|||
|
|
const prefix = '0'.repeat(difficulty);
|
|||
|
|
const t0 = Date.now();
|
|||
|
|
for (let nonce = 0; nonce < 5_000_000; nonce++) {
|
|||
|
|
if (sha256Hex(challenge + nonce).startsWith(prefix)) {
|
|||
|
|
return { nonce, ms: Date.now() - t0, tries: nonce + 1 };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
throw new Error('no solution');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function j(path, init) {
|
|||
|
|
const r = await fetch(BASE + path, init);
|
|||
|
|
const text = await r.text();
|
|||
|
|
try {
|
|||
|
|
return { status: r.status, body: JSON.parse(text) };
|
|||
|
|
} catch {
|
|||
|
|
return { status: r.status, body: text.slice(0, 200) };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
console.log('--- 1) GET /human/challenge');
|
|||
|
|
const ch = await j('/challenge');
|
|||
|
|
console.log(' ', ch.status, JSON.stringify(ch.body).slice(0, 160));
|
|||
|
|
|
|||
|
|
if (!ch.body || ch.body.enabled !== true) {
|
|||
|
|
console.log('开关没开(human_check 不是 1),先设置 KV 再测');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('--- 2) POST /human/verify 错误 PoW(应 need_captcha)');
|
|||
|
|
const bad = await j('/verify', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
challenge: ch.body.challenge,
|
|||
|
|
nonce: 12345,
|
|||
|
|
exp: ch.body.exp,
|
|||
|
|
sig: ch.body.sig,
|
|||
|
|
elapsedMs: 5000,
|
|||
|
|
events: 30,
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
console.log(' ', bad.status, JSON.stringify(bad.body));
|
|||
|
|
|
|||
|
|
console.log('--- 3) 正确 PoW + 无交互信号(应 need_click)');
|
|||
|
|
const sol = solve(ch.body.challenge, POW_DIFFICULTY);
|
|||
|
|
console.log(` 解出 nonce=${sol.nonce}(${sol.tries} 次尝试 / ${sol.ms}ms)`);
|
|||
|
|
const mid = await j('/verify', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
challenge: ch.body.challenge,
|
|||
|
|
nonce: sol.nonce,
|
|||
|
|
exp: ch.body.exp,
|
|||
|
|
sig: ch.body.sig,
|
|||
|
|
elapsedMs: 300,
|
|||
|
|
events: 0,
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
console.log(' ', mid.status, JSON.stringify(mid.body));
|
|||
|
|
|
|||
|
|
console.log('--- 4) 蜜罐被填(应 need_captcha)');
|
|||
|
|
const honey = await j('/verify', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
challenge: ch.body.challenge,
|
|||
|
|
nonce: sol.nonce,
|
|||
|
|
exp: ch.body.exp,
|
|||
|
|
sig: ch.body.sig,
|
|||
|
|
elapsedMs: 8000,
|
|||
|
|
events: 12,
|
|||
|
|
honeypot: 'bot@example.com',
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
console.log(' ', honey.status, JSON.stringify(honey.body));
|
|||
|
|
|
|||
|
|
console.log('--- 5) 正确 PoW + 有交互(应 pass:true 并下发通行证)');
|
|||
|
|
const okRes = await j('/verify', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json' },
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
challenge: ch.body.challenge,
|
|||
|
|
nonce: sol.nonce,
|
|||
|
|
exp: ch.body.exp,
|
|||
|
|
sig: ch.body.sig,
|
|||
|
|
elapsedMs: 8000,
|
|||
|
|
events: 12,
|
|||
|
|
}),
|
|||
|
|
});
|
|||
|
|
console.log(' ', okRes.status, JSON.stringify(okRes.body));
|
|||
|
|
|
|||
|
|
console.log('--- 6) GET /human/status(应 pass:true,说明 KV 通行证生效)');
|
|||
|
|
const st = await j('/status');
|
|||
|
|
console.log(' ', st.status, JSON.stringify(st.body));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch((e) => console.error('ERR', e.message));
|