import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import vm from "node:vm"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DEFAULT_KWS_FILE = path.join( __dirname, "kws-11-0.0.1-obfuscated.5e0a90af726d8a7e.js", ); const MAX_EVENTS = Number(process.env.KS_KWS_MAX_EVENTS || 240); function sha16(value) { return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 16); } function preview(value) { try { if (value === null) return "null"; if (value === undefined) return "undefined"; if (typeof value === "string") { return value.length > 120 ? `${value.slice(0, 120)}...(len=${value.length})` : value; } if (typeof value === "number" || typeof value === "boolean") return value; if (typeof value === "function") return `[function ${value.name || "anonymous"}]`; if (Array.isArray(value)) return `[array len=${value.length}]`; return `[object ${Object.keys(value).slice(0, 8).join(",")}]`; } catch { return "[unpreviewable]"; } } function buildContext() { const events = []; const record = (type, detail = {}) => { if (events.length >= MAX_EVENTS) return; events.push({ type, ...detail }); }; const mockCache = new Map(); function mock(name = "mock") { if (mockCache.has(name)) return mockCache.get(name); const fn = function (...args) { record("call", { path: name, args: args.map(preview) }); return mock(`${name}()`); }; const proxy = new Proxy(fn, { get(_target, prop) { if (prop === Symbol.toPrimitive) return () => ""; if (prop === "toString") return () => `[mock ${name}]`; if (prop === "valueOf") return () => 0; if (prop === "then") return undefined; return mock(`${name}.${String(prop)}`); }, set(_target, prop, value) { record("mock-set", { path: `${name}.${String(prop)}`, value: preview(value) }); return true; }, apply(_target, _thisArg, args) { record("apply", { path: name, args: args.map(preview) }); return mock(`${name}()`); }, construct(_target, args) { record("new", { path: name, args: args.map(preview) }); return mock(`new ${name}`); }, has() { return true; }, }); mockCache.set(name, proxy); return proxy; } const storage = new Map(); const localStorage = { getItem(key) { const text = String(key); const value = storage.get(text) || null; record("localStorage.getItem", { key: text, value: preview(value) }); return value; }, setItem(key, value) { const text = String(key); storage.set(text, String(value)); record("localStorage.setItem", { key: text, value: preview(String(value)) }); }, removeItem(key) { storage.delete(String(key)); record("localStorage.removeItem", { key: String(key) }); }, clear() { storage.clear(); record("localStorage.clear"); }, }; class FakeXMLHttpRequest { open(method, url) { this.method = method; this.url = url; record("xhr.open", { method: preview(method), url: preview(url) }); } setRequestHeader(key, value) { record("xhr.setRequestHeader", { key: preview(key), value: preview(value) }); } send(body) { record("xhr.send", { url: preview(this.url), body: preview(body) }); this.readyState = 4; this.status = 200; this.responseText = "{}"; if (typeof this.onreadystatechange === "function") this.onreadystatechange(); if (typeof this.onload === "function") this.onload(); } addEventListener(name, cb) { record("xhr.addEventListener", { name: preview(name), cb: preview(cb) }); } } let kwscode = ""; const rawWindow = {}; const windowProxy = new Proxy(rawWindow, { get(target, prop) { if (prop in target) return target[prop]; const key = String(prop); const value = mock(`window.${key}`); target[prop] = value; record("window.get-unknown", { key }); return value; }, set(target, prop, value) { target[prop] = value; record("window.set", { key: String(prop), value: preview(value) }); return true; }, has() { return true; }, }); Object.assign(rawWindow, { window: windowProxy, self: windowProxy, top: windowProxy, parent: windowProxy, globalThis: windowProxy, Object, Array, String, Function, Number, Boolean, Date, Math, RegExp, Error, TypeError, Promise, JSON, Infinity, undefined, Uint8Array, parseInt, parseFloat, isNaN, escape, encodeURI, encodeURIComponent, decodeURI, decodeURIComponent, Window: function Window() {}, Navigator: function Navigator() {}, Location: function Location() {}, kwscb(value) { kwscode = String(value || ""); record("kwscb", { length: kwscode.length, sha16: sha16(kwscode) }); }, location: { href: "https://nebula.kuaishou.com/nebula/task/earning?layoutType=4&source=bottom_guide_first", origin: "https://nebula.kuaishou.com", protocol: "https:", host: "nebula.kuaishou.com", hostname: "nebula.kuaishou.com", pathname: "/nebula/task/earning", search: "?layoutType=4&source=bottom_guide_first", }, document: { cookie: "kpn=NEBULA; kpf=ANDROID_PHONE; userId=0; did=ANDROID_FAKE; kuaishou.h5_st=FAKE", referrer: "", createElement(tag) { record("document.createElement", { tag: preview(tag) }); return mock(`element.${tag}`); }, getElementsByTagName(tag) { record("document.getElementsByTagName", { tag: preview(tag) }); return [mock(`tag.${tag}`)]; }, addEventListener(name, cb) { record("document.addEventListener", { name: preview(name), cb: preview(cb) }); }, body: mock("document.body"), head: mock("document.head"), documentElement: mock("document.documentElement"), }, navigator: { userAgent: "Mozilla/5.0 (Linux; Android 16; PJZ110 Build/BP2A.250605.015; wv) AppleWebKit/537.36", language: "zh-CN", languages: ["zh-CN", "zh"], platform: "Linux armv8l", webdriver: false, }, screen: { width: 1080, height: 2376, colorDepth: 24, pixelDepth: 24 }, localStorage, sessionStorage: localStorage, performance: { now: () => 1234.5, timing: {}, getEntriesByType: () => [] }, crypto: { getRandomValues(arr) { for (let i = 0; i < arr.length; i += 1) arr[i] = (i * 17 + 3) & 0xff; return arr; }, }, Image: function Image() { return mock("new Image"); }, XMLHttpRequest: FakeXMLHttpRequest, fetch(url, init) { record("fetch", { url: preview(url), init: preview(init) }); return Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve("{}"), json: () => Promise.resolve({}), }); }, setTimeout(cb) { record("setTimeout", { cb: preview(cb) }); if (typeof cb === "function") cb(); return 1; }, clearTimeout() {}, setInterval(cb) { record("setInterval", { cb: preview(cb) }); return 1; }, clearInterval() {}, console: { log: (...args) => record("console.log", { args: args.map(preview) }), warn: (...args) => record("console.warn", { args: args.map(preview) }), error: (...args) => record("console.error", { args: args.map(preview) }), }, }); const context = vm.createContext(windowProxy, { name: "h5-kws-sign", codeGeneration: { strings: true, wasm: false }, }); return { context, events, rawWindow, getKwscode: () => kwscode, }; } export function runKwsSignScript(scriptPath = DEFAULT_KWS_FILE, timeoutMs = 5000) { const { context, events, rawWindow, getKwscode } = buildContext(); const code = fs.readFileSync(scriptPath, "utf8"); const beforeKeys = new Set(Object.keys(rawWindow)); vm.runInContext(code, context, { filename: scriptPath, timeout: timeoutMs }); const kwscode = getKwscode(); if (!kwscode) { throw new Error("KWS script did not call kwscb()"); } const eventTypeCounts = {}; for (const event of events) eventTypeCounts[event.type] = (eventTypeCounts[event.type] || 0) + 1; return { ok: true, kwscode, length: kwscode.length, sha16: sha16(kwscode), newWindowKeys: Object.keys(rawWindow).filter((key) => !beforeKeys.has(key)), eventTypeCounts, }; } async function main() { const scriptPath = process.argv[2] || DEFAULT_KWS_FILE; const timeoutMs = Number(process.env.KS_KWS_VM_TIMEOUT_MS || 5000); try { const result = runKwsSignScript(scriptPath, timeoutMs); console.log(JSON.stringify(result)); } catch (err) { const message = String(err && err.stack ? err.stack : err); console.log(JSON.stringify({ ok: false, error: message.split(/\r?\n/).slice(0, 12).join("\n") })); process.exitCode = 1; } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { await main(); }