|
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 };
|
|
|