mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
cap 与系统信息
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Cap PoW (Proof-of-Work) 人机验证前端实现
|
||||
* 与后端 internal/util/cap 算法完全对应
|
||||
*
|
||||
* 求解在 Web Worker 中执行,不阻塞主线程 UI。
|
||||
*/
|
||||
|
||||
// ——— Challenge / Redeem API types ———
|
||||
|
||||
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 后端 internal/util/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 }),
|
||||
});
|
||||
if (!challengeRes.ok) {
|
||||
throw new Error('获取人机验证难题失败');
|
||||
}
|
||||
const challenge: ChallengeResponse = await challengeRes.json();
|
||||
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: RedeemResponse = await redeemRes.json();
|
||||
if (!redeemData.success || !redeemData.token) {
|
||||
throw new Error(redeemData.error || '人机验证失败');
|
||||
}
|
||||
|
||||
return redeemData.token;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ListUsersRequest,
|
||||
ListUsersResponse,
|
||||
SystemConfig,
|
||||
SystemStatus,
|
||||
TaskExecution,
|
||||
TaskMeta,
|
||||
TaskTypeResponse,
|
||||
@@ -318,4 +319,14 @@ export class AdminService extends BaseService {
|
||||
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
|
||||
return this.post<AdminUser>('/users', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统状态
|
||||
* @returns 系统状态指标数据
|
||||
* @throws {UnauthorizedError} 当未登录时
|
||||
* @throws {ForbiddenError} 当无管理员权限时
|
||||
*/
|
||||
static async getSystemStatus(): Promise<SystemStatus> {
|
||||
return this.get<SystemStatus>('/status');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,5 @@ export type {
|
||||
ListUsersRequest,
|
||||
ListUsersResponse,
|
||||
UpdateUserStatusRequest,
|
||||
SystemStatus,
|
||||
} from './types';
|
||||
|
||||
@@ -254,3 +254,37 @@ export interface AuthSourceRequest {
|
||||
export interface ToggleAuthSourceRequest {
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统状态信息
|
||||
*/
|
||||
export interface SystemStatus {
|
||||
uptime: string;
|
||||
num_goroutine: number;
|
||||
alloc: string;
|
||||
total_alloc: string;
|
||||
sys: string;
|
||||
lookups: number;
|
||||
mallocs: number;
|
||||
frees: number;
|
||||
heap_alloc: string;
|
||||
heap_sys: string;
|
||||
heap_idle: string;
|
||||
heap_inuse: string;
|
||||
heap_released: string;
|
||||
heap_objects: number;
|
||||
stack_inuse: string;
|
||||
stack_sys: string;
|
||||
mspan_inuse: string;
|
||||
mspan_sys: string;
|
||||
mcache_inuse: string;
|
||||
mcache_sys: string;
|
||||
buck_hash_sys: string;
|
||||
gc_sys: string;
|
||||
other_sys: string;
|
||||
next_gc: string;
|
||||
last_gc_time: string;
|
||||
pause_total_ns: string;
|
||||
last_pause: string;
|
||||
num_gc: number;
|
||||
}
|
||||
|
||||
@@ -103,8 +103,8 @@ export class AuthService extends BaseService {
|
||||
await this.get<void>('/oauth/logout');
|
||||
}
|
||||
|
||||
static async login(request: LoginRequest): Promise<User> {
|
||||
return this.post<User>('/user/login', request);
|
||||
static async login(request: LoginRequest, headers?: Record<string, string>): Promise<User> {
|
||||
return this.post<User>('/user/login', request, headers ? { headers } as any : undefined);
|
||||
}
|
||||
|
||||
static async register(request: RegisterRequest): Promise<User> {
|
||||
|
||||
@@ -16,4 +16,8 @@ export interface PublicConfigResponse {
|
||||
oidc_login_enabled: boolean;
|
||||
/** 每个用户最大 API Key 数量 */
|
||||
max_api_keys_per_user: number;
|
||||
/** 是否启用 Cap 人机验证 */
|
||||
cap_login_enabled: boolean;
|
||||
/** 是否自动解题 */
|
||||
cap_auto_solve: boolean;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import axios, { AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig } from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { apiConfig } from './config';
|
||||
import axios, {AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig} from 'axios';
|
||||
import {toast} from 'sonner';
|
||||
import {apiConfig} from './config';
|
||||
import {
|
||||
ApiErrorBase,
|
||||
NetworkError,
|
||||
TimeoutError,
|
||||
ForbiddenError,
|
||||
NetworkError,
|
||||
NotFoundError,
|
||||
ServerError,
|
||||
TimeoutError,
|
||||
ValidationError,
|
||||
} from './errors';
|
||||
import { ApiError, ApiResponse } from './types';
|
||||
import {ApiError, ApiResponse} from './types';
|
||||
|
||||
/**
|
||||
* API 客户端实例
|
||||
@@ -102,6 +102,11 @@ apiClient.interceptors.response.use(
|
||||
cancelTokens.delete(requestKey);
|
||||
pendingRequests.delete(requestKey);
|
||||
|
||||
const resData = response.data as any;
|
||||
if (resData && resData.error_msg) {
|
||||
return Promise.reject(new ApiErrorBase(resData.error_msg));
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<ApiError>) => {
|
||||
|
||||
@@ -105,6 +105,7 @@ export type {
|
||||
ListUsersRequest,
|
||||
ListUsersResponse,
|
||||
UpdateUserStatusRequest,
|
||||
SystemStatus,
|
||||
} from './admin';
|
||||
|
||||
// 用户服务
|
||||
|
||||
Reference in New Issue
Block a user