388 lines
10 KiB
JavaScript
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");
|
|
}
|
|
});
|