ksjsb/core/h5_kww_server.mjs
2026-07-30 20:25:56 +08:00

388 lines
10 KiB
JavaScript

import fs from "fs";
import readline from "readline";
import vm from "vm";
const KWF_PATH = new URL("./kwf-0.0.2.2cee19b4b7dec496.js", import.meta.url);
let fixedNow = process.env.KS_H5_KWW_NOW ? Number(process.env.KS_H5_KWW_NOW) : null;
let rngState = (process.env.KS_H5_KWW_SEED ? Number(process.env.KS_H5_KWW_SEED) : Date.now()) >>> 0;
let traceEvents = null;
let traceCounts = null;
const TRACE_TYPE_LIMITS = {
"Math.floor": 256,
"String.fromCharCode": 512,
"Object.assign": 64,
"Array.join": 64,
};
function trace(type, detail = {}) {
if (!traceEvents) return;
if (traceCounts) traceCounts[type] = (traceCounts[type] || 0) + 1;
const limit = TRACE_TYPE_LIMITS[type] || 2000;
if (traceCounts && traceCounts[type] > limit) return;
if (traceEvents.length >= 4000) return;
traceEvents.push({ type, ...detail });
}
function previewArgs(values, limit = 32) {
const arr = Array.from(values || []);
return {
count: arr.length,
first: arr.slice(0, limit),
truncated: arr.length > limit,
};
}
function seededRandom() {
rngState = (Math.imul(rngState, 1664525) + 1013904223) >>> 0;
const value = rngState / 0x100000000;
trace("random", { value });
return value;
}
function currentNow() {
const value = fixedNow === null || Number.isNaN(fixedNow) ? Date.now() : fixedNow;
trace("now", { value });
return value;
}
function FakeDate(...args) {
if (new.target) {
return args.length > 0 ? new Date(...args) : new Date(currentNow());
}
return new Date(args.length > 0 ? args[0] : currentNow()).toString();
}
Object.setPrototypeOf(FakeDate, Date);
FakeDate.prototype = Date.prototype;
FakeDate.now = () => currentNow();
FakeDate.parse = Date.parse;
FakeDate.UTC = Date.UTC;
const fakeMath = Object.create(Math);
fakeMath.random = () => seededRandom();
fakeMath.floor = (value) => {
const result = Math.floor(value);
trace("Math.floor", { value, result });
return result;
};
const NativeString = String;
function TracedString(...args) {
return NativeString(...args);
}
Object.setPrototypeOf(TracedString, NativeString);
TracedString.prototype = NativeString.prototype;
TracedString.fromCharCode = (...codes) => {
const result = NativeString.fromCharCode(...codes);
trace("String.fromCharCode", {
args: previewArgs(codes),
result: preview(result),
});
return result;
};
const NativeObject = Object;
function previewObject(value) {
if (!value || typeof value !== "object") return preview(value);
const keys = NativeObject.keys(value);
const sample = {};
for (const key of keys.slice(0, 16)) sample[key] = preview(value[key]);
return { keys: keys.slice(0, 32), sample, keyCount: keys.length };
}
function TracedObject(value) {
return NativeObject(value);
}
NativeObject.setPrototypeOf(TracedObject, NativeObject);
TracedObject.prototype = NativeObject.prototype;
for (const name of NativeObject.getOwnPropertyNames(NativeObject)) {
if (name === "length" || name === "name" || name === "prototype") continue;
NativeObject.defineProperty(
TracedObject,
name,
NativeObject.getOwnPropertyDescriptor(NativeObject, name),
);
}
TracedObject.assign = (target, ...sources) => {
const result = NativeObject.assign(target, ...sources);
trace("Object.assign", {
sourceCount: sources.length,
sources: sources.map((item) => previewObject(item)),
result: previewObject(result),
});
return result;
};
const nativeArrayJoin = Array.prototype.join;
Array.prototype.join = function tracedJoin(separator) {
const result = nativeArrayJoin.call(this, separator);
if (separator === "|" || (this.length === 14 && result.includes("|"))) {
trace("Array.join", {
separator: separator === undefined ? "," : String(separator),
length: this.length,
result: preview(result),
});
}
return result;
};
const mockCache = new Map();
function preview(value) {
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") {
const text = NativeString(value);
return text.length > 160 ? `${text.slice(0, 160)}...(len=${text.length})` : text;
}
if (Array.isArray(value)) return `[array len=${value.length}]`;
try {
return `[object ${Object.keys(value).slice(0, 8).join(",")}]`;
} catch {
return "[unpreviewable]";
}
}
function mock(path = "mock") {
if (mockCache.has(path)) return mockCache.get(path);
const fn = function () {
return mock(`${path}()`);
};
const proxy = new Proxy(fn, {
get(_target, prop) {
if (prop === Symbol.toPrimitive) return () => "";
if (prop === "toString") return () => `[mock ${path}]`;
if (prop === "valueOf") return () => 0;
if (prop === "then") return undefined;
return mock(`${path}.${String(prop)}`);
},
set() {
return true;
},
apply() {
return mock(`${path}()`);
},
construct() {
return mock(`new ${path}`);
},
has() {
return true;
},
});
mockCache.set(path, proxy);
return proxy;
}
const storage = new Map();
const localStorage = {
getItem(key) {
const value = storage.get(String(key)) || null;
trace("localStorage.getItem", { key: String(key), value });
return value;
},
setItem(key, value) {
trace("localStorage.setItem", { key: String(key), value: String(value) });
storage.set(String(key), String(value));
},
removeItem(key) {
trace("localStorage.removeItem", { key: String(key) });
storage.delete(String(key));
},
};
class FakeXMLHttpRequest {
open(method, url) {
this.method = method;
this.url = url;
}
setRequestHeader() {}
send() {
this.readyState = 4;
this.status = 200;
this.responseText = "{}";
if (typeof this.onreadystatechange === "function") this.onreadystatechange();
if (typeof this.onload === "function") this.onload();
}
addEventListener() {}
}
const rawWindow = {};
const windowProxy = new Proxy(rawWindow, {
get(target, prop) {
if (prop in target) return target[prop];
const value = mock(`window.${String(prop)}`);
target[prop] = value;
return value;
},
set(target, prop, value) {
target[prop] = value;
return true;
},
has() {
return true;
},
});
Object.assign(rawWindow, {
window: windowProxy,
self: windowProxy,
top: windowProxy,
parent: windowProxy,
Object: TracedObject,
Array,
String: TracedString,
Function,
Number,
Boolean,
Date: FakeDate,
Math: fakeMath,
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() {},
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: "",
referrer: "",
createElement(tag) {
return mock(`element.${tag}`);
},
getElementsByTagName(tag) {
return [mock(`tag.${tag}`)];
},
addEventListener() {},
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: () => currentNow() % 100000, timing: {}, getEntriesByType: () => [] },
crypto: {
getRandomValues(arr) {
for (let i = 0; i < arr.length; i += 1) arr[i] = Math.floor(seededRandom() * 256);
return arr;
},
},
XMLHttpRequest: FakeXMLHttpRequest,
fetch() {
return Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve("{}"), json: () => Promise.resolve({}) });
},
setTimeout(cb) {
if (typeof cb === "function") cb();
return 1;
},
clearTimeout() {},
setInterval() {
return 1;
},
clearInterval() {},
console: {
log() {},
warn() {},
error() {},
},
});
rawWindow.globalThis = windowProxy;
const context = vm.createContext(windowProxy, {
name: "h5-kww",
codeGeneration: { strings: true, wasm: false },
});
function loadKwf() {
const code = fs.readFileSync(KWF_PATH, "utf8");
vm.runInContext(code, context, { filename: KWF_PATH.pathname, timeout: 5000 });
if (!rawWindow.kwpsec || typeof rawWindow.kwpsec.getData !== "function") {
throw new Error(`kwpsec.getData not installed, kwpsec=${preview(rawWindow.kwpsec)}`);
}
}
function setCookie(cookie) {
rawWindow.document.cookie = String(cookie || "");
}
function buildArg(req) {
if (req && req.arg !== undefined) return req.arg;
if (req && req.url) {
return {
url: String(req.url),
method: String(req.method || "GET"),
headers: req.headers || {},
body: req.body || "",
data: req.body || "",
};
}
return undefined;
}
function makeKww(req) {
traceEvents = req.trace ? [] : null;
traceCounts = req.trace ? {} : null;
setCookie(req.cookie || "");
const arg = buildArg(req);
const getData = rawWindow.kwpsec.getData;
const value = arg === undefined ? getData.call(rawWindow.kwpsec) : getData.call(rawWindow.kwpsec, arg);
const kww = String(value || "");
if (!kww) {
throw new Error("kwpsec.getData returned empty value");
}
return {
kww,
kwfcv1: storage.get("kwfcv1") || "",
kwfv1: storage.get("kwfv1") || "",
trace: traceEvents || undefined,
traceSummary: traceCounts || undefined,
};
}
loadKwf();
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
rl.on("line", (line) => {
let id = null;
try {
const req = JSON.parse(line);
id = req.id ?? null;
const result = makeKww(req);
process.stdout.write(JSON.stringify({ id, ok: true, ...result }) + "\n");
} catch (err) {
process.stdout.write(JSON.stringify({ id, ok: false, error: String(err && err.stack ? err.stack : err) }) + "\n");
}
});