mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
23a5488203
Drop internal/util (unused httppool wrapper and dead StringArray) and rely on pkg/httppool plus oauth context injection for HTTP clients.
189 lines
5.6 KiB
TypeScript
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;
|
|
}
|