feat: add proxy log billing details handling and schema management

This commit is contained in:
cita
2026-03-06 18:12:12 +08:00
parent bf216d7011
commit 3748e6dcb6
11 changed files with 788 additions and 29 deletions
@@ -0,0 +1,255 @@
import type { RequestInit as UndiciRequestInit } from 'undici';
import { createContext, runInContext } from 'node:vm';
import { withSiteProxyRequestInit } from '../siteProxy.js';
const SHIELD_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36';
export function buildNewApiCookieCandidates(token: string): string[] {
const trimmed = (token || '').trim();
if (!trimmed) return [];
const raw = trimmed.startsWith('Bearer ') ? trimmed.slice(7).trim() : trimmed;
const candidates: string[] = [];
if (raw.includes('=')) {
candidates.push(raw);
}
candidates.push(`session=${raw}`);
candidates.push(`token=${raw}`);
return Array.from(new Set(candidates));
}
export function hasUsableSessionCookie(cookieHeader: string): boolean {
if (!cookieHeader) return false;
const ignored = new Set(['acw_tc', 'acw_sc__v2', 'cdn_sec_tc']);
const pairs = cookieHeader.split(';').map((part) => part.trim()).filter(Boolean);
for (const pair of pairs) {
const eq = pair.indexOf('=');
if (eq <= 0) continue;
const name = pair.slice(0, eq).trim().toLowerCase();
if (!name || ignored.has(name)) continue;
if (
name === 'session'
|| name === 'token'
|| name === 'auth_token'
|| name === 'access_token'
|| name === 'jwt'
|| name === 'jwt_token'
|| name.includes('session')
|| name.includes('token')
|| name.includes('auth')
) {
return true;
}
}
return false;
}
function parseChallengeArg1(html: string): string | null {
const match = html.match(/var\s+arg1\s*=\s*['"]([0-9a-fA-F]+)['"]/);
return match?.[1]?.toUpperCase() || null;
}
function parseChallengeMapping(html: string): number[] | null {
const match = html.match(/for\(var m=\[([^\]]+)\],p=L\(0x115\)/);
if (!match?.[1]) return null;
const values = match[1].split(',').map((raw) => {
const value = raw.trim().toLowerCase();
if (!value) return Number.NaN;
if (value.startsWith('0x')) return Number.parseInt(value.slice(2), 16);
return Number.parseInt(value, 10);
});
if (values.some((value) => Number.isNaN(value))) return null;
return values;
}
function parseChallengeXorSeed(html: string): string | null {
const fnStart = html.indexOf('function a0i()');
const bStart = html.indexOf('function b(');
const rotateStart = html.indexOf('(function(a,c){');
const rotateEnd = html.indexOf('),!(function', rotateStart);
if (fnStart < 0 || bStart < 0 || bStart <= fnStart || rotateStart < 0 || rotateEnd < 0) {
return null;
}
const helperCode = html.slice(fnStart, bStart);
const rotateCode = `${html.slice(rotateStart, rotateEnd + 1)})`;
try {
const sandbox: Record<string, unknown> = { decodeURIComponent };
createContext(sandbox);
runInContext(helperCode, sandbox, { timeout: 100 });
runInContext(rotateCode, sandbox, { timeout: 100 });
const decoder = sandbox.a0j;
if (typeof decoder !== 'function') return null;
const seed = (decoder as (index: number) => unknown)(0x115);
if (typeof seed !== 'string' || !/^[0-9a-f]+$/i.test(seed)) return null;
return seed;
} catch {
return null;
}
}
export function solveNewApiAcwScV2(html: string): string | null {
const arg1 = parseChallengeArg1(html);
const mapping = parseChallengeMapping(html);
const xorSeed = parseChallengeXorSeed(html);
if (!arg1 || !mapping || !xorSeed) return null;
const reordered: string[] = [];
for (let i = 0; i < arg1.length; i += 1) {
const ch = arg1[i];
for (let j = 0; j < mapping.length; j += 1) {
if (mapping[j] === i + 1) {
reordered[j] = ch;
}
}
}
const source = reordered.join('');
let out = '';
for (let i = 0; i < source.length && i < xorSeed.length; i += 2) {
const left = Number.parseInt(source.slice(i, i + 2), 16);
const right = Number.parseInt(xorSeed.slice(i, i + 2), 16);
if (Number.isNaN(left) || Number.isNaN(right)) return null;
out += (left ^ right).toString(16).padStart(2, '0');
}
return out || null;
}
function isShieldChallenge(contentType: string, text: string): boolean {
const normalizedType = (contentType || '').toLowerCase();
if (normalizedType.includes('text/html') && /var\s+arg1\s*=|acw_sc__v2|cdn_sec_tc|<script/i.test(text)) {
return true;
}
return /var\s+arg1\s*=/.test(text);
}
function normalizeHeaders(headers?: UndiciRequestInit['headers']): Record<string, string> {
const output: Record<string, string> = {};
if (!headers) return output;
if (Array.isArray(headers)) {
for (const [key, value] of headers) {
output[String(key)] = String(value);
}
return output;
}
const maybeIterable = headers as { forEach?: (fn: (value: string, key: string) => void) => void };
if (typeof maybeIterable.forEach === 'function') {
maybeIterable.forEach((value, key) => {
output[key] = value;
});
return output;
}
for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {
if (value === undefined || value === null) continue;
output[key] = String(value);
}
return output;
}
function upsertCookie(cookieHeader: string, name: string, value: string): string {
const parts = cookieHeader.split(';').map((part) => part.trim()).filter(Boolean);
let replaced = false;
const next = parts.map((part) => {
const eq = part.indexOf('=');
if (eq < 0) return part;
const key = part.slice(0, eq).trim();
if (key !== name) return part;
replaced = true;
return `${name}=${value}`;
});
if (!replaced) next.push(`${name}=${value}`);
return next.join('; ');
}
function mergeSetCookiePairs(cookieHeader: string, setCookieHeaders: string[]): string {
let merged = cookieHeader;
for (const raw of setCookieHeaders) {
if (!raw) continue;
const firstPair = raw.split(';')[0]?.trim();
if (!firstPair) continue;
const eq = firstPair.indexOf('=');
if (eq <= 0) continue;
const name = firstPair.slice(0, eq).trim();
const value = firstPair.slice(eq + 1);
merged = upsertCookie(merged, name, value);
}
return merged;
}
function parseJsonSafe<T>(text: string): T | null {
try {
return JSON.parse(text) as T;
} catch {
return null;
}
}
function collectSetCookieHeaders(headers: Headers): string[] {
const getSetCookie = (headers as unknown as { getSetCookie?: () => string[] }).getSetCookie;
if (typeof getSetCookie === 'function') {
return getSetCookie.call(headers) || [];
}
const single = headers.get('set-cookie');
return single ? [single] : [];
}
export async function fetchJsonWithShieldCookieRetry<T>(
url: string,
options?: UndiciRequestInit,
): Promise<{ data: T | null; cookieHeader: string }> {
const { fetch } = await import('undici');
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'User-Agent': SHIELD_USER_AGENT,
...normalizeHeaders(options?.headers),
};
let cookieHeader = headers.Cookie || headers.cookie || '';
if (cookieHeader) {
headers.Cookie = cookieHeader;
delete headers.cookie;
}
for (let attempt = 0; attempt < 3; attempt += 1) {
const requestOptions: UndiciRequestInit = {
...options,
body: options?.body ?? undefined,
headers,
};
const proxiedRequestOptions = await withSiteProxyRequestInit(url, requestOptions);
const response = await fetch(url, proxiedRequestOptions);
const text = await response.text();
cookieHeader = mergeSetCookiePairs(cookieHeader, collectSetCookieHeaders(response.headers));
const parsed = parseJsonSafe<T>(text);
if (parsed) return { data: parsed, cookieHeader };
if (!isShieldChallenge(response.headers.get('content-type') || '', text)) {
return { data: null, cookieHeader };
}
if (!cookieHeader) {
return { data: null, cookieHeader };
}
const acwScV2 = solveNewApiAcwScV2(text);
if (!acwScV2) {
return { data: null, cookieHeader };
}
cookieHeader = upsertCookie(cookieHeader, 'acw_sc__v2', acwScV2);
headers.Cookie = cookieHeader;
}
return { data: null, cookieHeader };
}