|
import { JSDOM } from "npm:jsdom"; |
|
import { userAgent } from "./utils.ts"; |
|
import crypto from "node:crypto"; |
|
|
|
|
|
interface MockMeta { |
|
readonly origin: string; |
|
readonly stack: string; |
|
readonly duration: number; |
|
} |
|
|
|
interface HashResults { |
|
server_hashes: string[]; |
|
client_hashes: string[]; |
|
signals: Record<string, unknown>; |
|
meta: Record<string, unknown>; |
|
} |
|
|
|
interface JSExecutionResult { |
|
server_hashes?: string[]; |
|
client_hashes?: string[]; |
|
signals?: Record<string, unknown>; |
|
meta?: Record<string, unknown>; |
|
} |
|
|
|
const mockMeta: MockMeta = { |
|
origin: "https://duckduckgo.com", |
|
stack: "", |
|
duration: Math.floor(Math.random() * 100) + 1, |
|
} as const; |
|
|
|
class VqdHashGenerator { |
|
private static extractIIFEJSDOM(jsCode: string): HashResults { |
|
const results: HashResults = { |
|
server_hashes: [], |
|
client_hashes: [], |
|
signals: {}, |
|
meta: {}, |
|
}; |
|
|
|
let dom: JSDOM | null = null; |
|
|
|
try { |
|
dom = new JSDOM( |
|
`<!DOCTYPE html><html><head></head><body></body></html>`, |
|
{ |
|
url: "https://duckduckgo.com", |
|
referrer: "https://duckduckgo.com", |
|
pretendToBeVisual: true, |
|
resources: "usable", |
|
} |
|
); |
|
|
|
const { window } = dom; |
|
|
|
|
|
globalThis.window = window; |
|
globalThis.document = window.document; |
|
|
|
const customNavigator = { |
|
userAgent: userAgent, |
|
platform: "Win32", |
|
language: "en-US", |
|
languages: ["en-US", "en"], |
|
cookieEnabled: true, |
|
onLine: true, |
|
hardwareConcurrency: 4, |
|
maxTouchPoints: 0, |
|
vendor: "Google Inc.", |
|
vendorSub: "", |
|
productSub: "20030107", |
|
appName: "Netscape", |
|
appVersion: userAgent, |
|
product: "Gecko", |
|
}; |
|
|
|
Object.defineProperty(window, "navigator", { |
|
value: customNavigator, |
|
writable: true, |
|
configurable: true, |
|
enumerable: true, |
|
}); |
|
|
|
Object.defineProperty(globalThis, "navigator", { |
|
value: customNavigator, |
|
writable: true, |
|
configurable: true, |
|
enumerable: true, |
|
}); |
|
|
|
delete (window.navigator as any).webdriver; |
|
|
|
|
|
const result = window.eval(jsCode) as JSExecutionResult; |
|
|
|
if (result && typeof result === "object") { |
|
results.server_hashes = result.server_hashes || []; |
|
results.client_hashes = result.client_hashes || []; |
|
results.signals = result.signals || {}; |
|
results.meta = result.meta || {}; |
|
} |
|
} catch (error) { |
|
console.error("JSDOM execution failed:", error); |
|
throw new Error( |
|
`JSDOM执行失败: ${ |
|
error instanceof Error ? error.message : String(error) |
|
}` |
|
); |
|
} finally { |
|
|
|
if (dom) { |
|
dom.window.close(); |
|
} |
|
delete globalThis.window; |
|
delete globalThis.document; |
|
delete globalThis.navigator; |
|
} |
|
|
|
return results; |
|
} |
|
|
|
private static hashClientValues(mockClientHashes: string[]): string[] { |
|
return mockClientHashes.map((value: string): string => { |
|
const hash = crypto.createHash("sha256"); |
|
hash.update(value, "utf8"); |
|
return hash.digest("base64"); |
|
}); |
|
} |
|
|
|
static generate(vqdHashRequest: string): string { |
|
if (!vqdHashRequest || typeof vqdHashRequest !== "string") { |
|
throw new Error("VQD Hash 请求参数无效"); |
|
} |
|
|
|
try { |
|
|
|
const jsCode = atob(vqdHashRequest); |
|
|
|
|
|
const hash = this.extractIIFEJSDOM(jsCode); |
|
|
|
|
|
hash.client_hashes = this.hashClientValues(hash.client_hashes); |
|
|
|
|
|
hash.meta = { |
|
...hash.meta, |
|
...mockMeta, |
|
}; |
|
|
|
|
|
return btoa(JSON.stringify(hash)); |
|
} catch (error) { |
|
console.error("generateVqdHash 执行失败:", error); |
|
throw new Error( |
|
`VQD Hash 生成失败: ${ |
|
error instanceof Error ? error.message : String(error) |
|
}` |
|
); |
|
} |
|
} |
|
} |
|
|
|
|
|
export function generateVqdHash(vqdHashRequest: string): string { |
|
return VqdHashGenerator.generate(vqdHashRequest); |
|
} |
|
|
|
|
|
export { VqdHashGenerator }; |
|
export type { HashResults, MockMeta, JSExecutionResult }; |
|
|