Files
ryan 23a5488203 refactor(http): remove dead internal/util HTTP client wrapper
Drop internal/util (unused httppool wrapper and dead StringArray) and
rely on pkg/httppool plus oauth context injection for HTTP clients.
2026-07-24 15:49:52 +08:00

189 lines
5.6 KiB
TypeScript

/**
* Cap PoW (Proof-of-Work) 人机验证前端实现
* 与后端 pkg/cap 算法完全对应
*
* 求解在 Web Worker 中执行,不阻塞主线程 UI。
*/
// ——— Challenge / Redeem API types ———
import { readApiData } from '@/lib/api-envelope';
export interface ChallengeResponse {
challenge: { c: number; s: number; d: number };
token: string;
expires: number;
}
export interface RedeemResponse {
success: boolean;
token?: string;
expires?: number;
error?: string;
}
// ——— Worker 代码(内联 Blob,避免独立文件的打包配置问题)———
//
// 算法与 Go 后端 pkg/cap/cap.go + prng.go 完全对应:
// · FNV-1a 32-bit (Math.imul 保证 32-bit 截断)
// · xorshift32 PRNG → hex 字符串
// · SubtleCrypto SHA-256 校验答案
const WORKER_SOURCE = /* js */ `
// FNV-1a 32-bit
function fnv1a(str) {
let h = 2166136261 >>> 0;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return h;
}
function fnv1aResume(state, str) {
let h = state >>> 0;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return h;
}
// xorshift32 PRNG → hex string
function prngFromHash(seed, len) {
let s = seed >>> 0, r = '';
while (r.length < len) {
s ^= (s << 13) >>> 0;
s ^= (s >>> 17) >>> 0;
s ^= (s << 5) >>> 0;
s = s >>> 0;
r += s.toString(16).padStart(8, '0');
}
return r.slice(0, len);
}
// SHA-256 via SubtleCrypto (available in workers)
async function sha256Hex(input) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
// Solve one puzzle
async function solvePuzzle(token, index, size, difficulty) {
const tFnv = fnv1a(token);
const saltSeed = fnv1aResume(tFnv, String(index + 1));
const targetSeed = fnv1aResume(saltSeed, 'd');
const salt = prngFromHash(saltSeed, size);
const target = prngFromHash(targetSeed, difficulty);
for (let nonce = 0; nonce < 10_000_000; nonce++) {
if ((await sha256Hex(salt + nonce)).startsWith(target)) return nonce;
}
throw new Error('无法在合理范围内求解第 ' + (index + 1) + ' 个难题');
}
// Entry point — receive task from main thread
self.onmessage = async ({ data }) => {
const { token, count, size, difficulty } = data;
try {
const t0 = performance.now();
const solutions = [];
for (let i = 0; i < count; i++) {
solutions.push(await solvePuzzle(token, i, size, difficulty));
}
const elapsed = ((performance.now() - t0) / 1000).toFixed(3);
self.postMessage({ type: 'done', solutions, elapsed });
} catch (err) {
self.postMessage({ type: 'error', message: err.message });
}
};
`;
// ——— 在 Web Worker 中求解,返回 Promise<number[]> ———
function solveInWorker(
token: string,
count: number,
size: number,
difficulty: number,
): Promise<{ solutions: number[]; elapsed: string }> {
return new Promise((resolve, reject) => {
const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
worker.onmessage = (e) => {
URL.revokeObjectURL(url);
worker.terminate();
if (e.data.type === 'done') {
resolve({ solutions: e.data.solutions, elapsed: e.data.elapsed });
} else {
reject(new Error(e.data.message));
}
};
worker.onerror = (err) => {
URL.revokeObjectURL(url);
worker.terminate();
reject(new Error(err.message || 'Worker 执行失败'));
};
worker.postMessage({ token, count, size, difficulty });
});
}
// ——— Full Cap flow: get challenge → solve (Worker) → redeem → return X-Cap-Token ———
export async function getCapToken(scope = 'login'): Promise<string> {
// 1. 获取难题
const challengeRes = await fetch('/api/cap/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scope }),
});
const challenge = await readApiData<ChallengeResponse>(
challengeRes,
'获取人机验证难题失败',
);
const { c: count, s: size, d: difficulty } = challenge.challenge;
console.groupCollapsed('[Cap] 人机验证 PoW 求解开始');
console.log(` 难题数量 (count) : ${count}`);
console.log(` 验证难度 (difficulty): ${difficulty}`);
console.log(` 盐值长度 (size) : ${size}`);
console.groupEnd();
// 2. 在 Worker 中求解(不阻塞主线程)
const t0 = performance.now();
const { solutions, elapsed } = await solveInWorker(
challenge.token,
count,
size,
difficulty,
);
const wallTime = ((performance.now() - t0) / 1000).toFixed(3);
console.groupCollapsed('[Cap] 人机验证 PoW 求解完成');
console.log(` 难题数量 (count) : ${count}`);
console.log(` 验证难度 (difficulty): ${difficulty}`);
console.log(` Worker 耗时 : ${elapsed}s`);
console.log(` 总耗时(含通信) : ${wallTime}s`);
console.log(` Solutions :`, solutions);
console.groupEnd();
// 3. 提交答案兑换一次性凭证
const redeemRes = await fetch('/api/cap/redeem', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: challenge.token, solutions, scope }),
});
const redeemData = await readApiData<RedeemResponse>(
redeemRes,
'人机验证失败',
);
if (!redeemData.success || !redeemData.token) {
throw new Error(redeemData.error || '人机验证失败');
}
return redeemData.token;
}