wowdump 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/adapters.js +101 -0
- package/dist/agent.js +1335 -0
- package/dist/analysis-path.js +38 -0
- package/dist/analysis-process-log.js +146 -0
- package/dist/broker-client.js +411 -0
- package/dist/broker-codec.js +148 -0
- package/dist/broker-core.js +1045 -0
- package/dist/broker-gateway.js +447 -0
- package/dist/broker-ledger.js +196 -0
- package/dist/broker-main.js +291 -0
- package/dist/broker-protocol.js +119 -0
- package/dist/broker-runtime.js +1283 -0
- package/dist/broker-server.js +466 -0
- package/dist/build-bundle-loader.js +183 -0
- package/dist/build-bundle.js +11 -0
- package/dist/discovery.js +59 -0
- package/dist/dry-run.js +38 -0
- package/dist/error-log.js +71 -0
- package/dist/focus-errors.js +63 -0
- package/dist/focus-service.js +1855 -0
- package/dist/focused-session.js +1357 -0
- package/dist/frida-runtime.js +711 -0
- package/dist/mcp-main.js +51 -0
- package/dist/mcp.js +924 -0
- package/dist/observability.js +41 -0
- package/dist/process-log-lock.js +195 -0
- package/dist/processes.js +47 -0
- package/dist/runtime-config.js +399 -0
- package/dist/session.js +145 -0
- package/dist/storage.js +12 -0
- package/dist/types.js +26 -0
- package/dist/wow-analysis.js +1430 -0
- package/package.json +64 -13
- package/resources/builds/retail/12.0.7.68974/build-profile.json +290 -0
- package/resources/builds/retail/12.0.7.68974/data-sources.json +1633 -0
- package/resources/builds/retail/12.0.7.68974/lua-targets.jsonl +5130 -0
- package/resources/builds/retail/12.0.7.68974/manifest.json +63 -0
- package/resources/builds/retail/12.0.7.68974/signatures.json +260 -0
- package/index.js +0 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { BrokerCore } from "./broker-core.js";
|
|
6
|
+
import { BrokerServer, FileBrokerArtifactStore, FileBrokerSingletonGuard } from "./broker-server.js";
|
|
7
|
+
import { BrokerFridaRuntime } from "./broker-runtime.js";
|
|
8
|
+
import { FridaCommandRuntime } from "./frida-runtime.js";
|
|
9
|
+
import { canonicalArtifactRoot, sidHash } from "./broker-protocol.js";
|
|
10
|
+
import { isWindowsProcessElevated } from "./broker-client.js";
|
|
11
|
+
import { discoverInstalls } from "./discovery.js";
|
|
12
|
+
import { listWowProcesses } from "./processes.js";
|
|
13
|
+
import { FocusAnalysisService } from "./focus-service.js";
|
|
14
|
+
import { AnalysisProcessLog } from "./analysis-process-log.js";
|
|
15
|
+
process.title = "wowdump-broker";
|
|
16
|
+
if (process.env.NODE_ENV === "test" && process.env.WOW_BROKER_LIFECYCLE_NORMALIZE_FIXTURE) {
|
|
17
|
+
const fixture = JSON.parse(process.env.WOW_BROKER_LIFECYCLE_NORMALIZE_FIXTURE);
|
|
18
|
+
const event = fixture.event && typeof fixture.event === "object" && !Array.isArray(fixture.event) ? fixture.event : {};
|
|
19
|
+
const seq = Number.isSafeInteger(fixture.seq) ? Number(fixture.seq) : 1;
|
|
20
|
+
const fixtureInstanceId = typeof fixture.instanceId === "string" ? fixture.instanceId : "fixture-instance";
|
|
21
|
+
const timestamp = typeof fixture.timestamp === "string" ? fixture.timestamp : new Date().toISOString();
|
|
22
|
+
process.stdout.write(`${JSON.stringify(normalizeLifecycleEvent(event, seq, fixtureInstanceId, timestamp))}\n`);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
const configuredPipeName = argument("--pipe") ?? process.env.WOW_BROKER_PIPE;
|
|
26
|
+
if (!configuredPipeName)
|
|
27
|
+
throw new Error("Broker pipe name is required");
|
|
28
|
+
const pipeName = configuredPipeName;
|
|
29
|
+
const sid = process.env.WOW_BROKER_SID ?? process.env.USERNAME ?? "default";
|
|
30
|
+
const currentSidHash = argument("--sid-hash") ?? process.env.WOW_BROKER_SID_HASH ?? sidHash(sid);
|
|
31
|
+
if (!/^[a-f0-9]{24}$/i.test(currentSidHash))
|
|
32
|
+
throw new Error("Broker SID hash is invalid");
|
|
33
|
+
const bootstrapNonce = argument("--bootstrap-nonce") ?? process.env.WOW_BROKER_BOOTSTRAP_NONCE ?? randomUUID();
|
|
34
|
+
const instanceId = randomUUID();
|
|
35
|
+
const artifactDir = canonicalArtifactRoot(argument("--artifact-root") ?? process.env.WOW_BROKER_ARTIFACT_ROOT ?? process.env.WOWDUMP_RUNTIME_DIR ?? join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "wowdump"));
|
|
36
|
+
const profileRoot = resolve(process.env.WOWDUMP_PROFILE_DIR ?? artifactDir);
|
|
37
|
+
const gameRoots = (process.env.WOWDUMP_GAME_ROOTS ?? process.env.WOW_ROOT ?? "")
|
|
38
|
+
.split(";").map(value => value.trim()).filter(Boolean);
|
|
39
|
+
const executor = new FridaCommandRuntime({ artifactDir });
|
|
40
|
+
const processLog = new AnalysisProcessLog(artifactDir);
|
|
41
|
+
const focusService = new FocusAnalysisService({ executor, runtimeRoot: artifactDir, profileRoot, deferAttachmentDetach: true, lifecycle: event => appendLifecycle(event) });
|
|
42
|
+
const readyIdentity = { instanceId, protocolVersion: "2026-07-28", sidHash: currentSidHash, elevated: await isWindowsProcessElevated(), pipeName, bootstrapNonce, artifactRoot: artifactDir };
|
|
43
|
+
const processResolver = async (pid) => {
|
|
44
|
+
try {
|
|
45
|
+
const installs = gameRoots.flatMap(gameRoot => discoverInstalls(gameRoot));
|
|
46
|
+
const process = (await listWowProcesses(installs)).find(item => item.pid === pid);
|
|
47
|
+
return process ? {
|
|
48
|
+
pid: process.pid,
|
|
49
|
+
name: "Wow.exe",
|
|
50
|
+
executable: process.executable,
|
|
51
|
+
processStartTime: process.startTime,
|
|
52
|
+
buildKey: process.install.build.buildKey
|
|
53
|
+
} : undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const runtime = new BrokerFridaRuntime(executor, readyIdentity, undefined, join(artifactDir, "broker-evidence"), processResolver, focusService);
|
|
60
|
+
let server;
|
|
61
|
+
let core;
|
|
62
|
+
let closing;
|
|
63
|
+
async function start() {
|
|
64
|
+
core = new BrokerCore({
|
|
65
|
+
runtime,
|
|
66
|
+
ledgerPath: join(artifactDir, "frida-broker-request-ledger.jsonl"),
|
|
67
|
+
idleTimeoutMs: numericEnvironment("WOWDUMP_BROKER_IDLE_MS", 20 * 60 * 1000),
|
|
68
|
+
lifecycle: event => appendLifecycle(event),
|
|
69
|
+
closeServer: () => server?.closeServer(),
|
|
70
|
+
releaseSingleton: () => server?.releaseGuard()
|
|
71
|
+
});
|
|
72
|
+
const artifactStore = new FileBrokerArtifactStore(join(artifactDir, "broker-artifacts"));
|
|
73
|
+
const guard = new FileBrokerSingletonGuard(join(homedir(), `.wowdump-frida-${currentSidHash}.lock`), pipeName);
|
|
74
|
+
server = new BrokerServer({ pipeName, core, guard, artifactStore });
|
|
75
|
+
await server.listen();
|
|
76
|
+
}
|
|
77
|
+
async function stop(reason) {
|
|
78
|
+
if (closing)
|
|
79
|
+
return closing;
|
|
80
|
+
closing = (async () => {
|
|
81
|
+
if (core)
|
|
82
|
+
await core.shutdown(reason).catch(() => undefined);
|
|
83
|
+
else
|
|
84
|
+
await server?.close().catch(() => undefined);
|
|
85
|
+
})();
|
|
86
|
+
return closing;
|
|
87
|
+
}
|
|
88
|
+
process.once("SIGINT", () => void stop("signal"));
|
|
89
|
+
process.once("SIGTERM", () => void stop("signal"));
|
|
90
|
+
process.once("uncaughtException", error => {
|
|
91
|
+
console.error("broker:", error);
|
|
92
|
+
void stop("broker_fatal").finally(() => process.exit(1));
|
|
93
|
+
});
|
|
94
|
+
process.once("unhandledRejection", error => {
|
|
95
|
+
console.error("broker:", error);
|
|
96
|
+
void stop("broker_fatal").finally(() => process.exit(1));
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
await start();
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
await writeStartupFailure(error).catch(() => undefined);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
function argument(name) {
|
|
106
|
+
const index = process.argv.indexOf(name);
|
|
107
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
108
|
+
}
|
|
109
|
+
function numericEnvironment(name, fallback) {
|
|
110
|
+
const value = Number(process.env[name]);
|
|
111
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : fallback;
|
|
112
|
+
}
|
|
113
|
+
async function writeStartupFailure(error) {
|
|
114
|
+
await mkdir(artifactDir, { recursive: true });
|
|
115
|
+
const safeNonce = bootstrapNonce.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 128);
|
|
116
|
+
const path = join(artifactDir, `broker-startup-failure-${safeNonce}.json`);
|
|
117
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
118
|
+
const protocolError = error;
|
|
119
|
+
const report = {
|
|
120
|
+
schema: "wow.broker-startup-failure.v1",
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
pid: process.pid,
|
|
123
|
+
pipeName,
|
|
124
|
+
sidHash: currentSidHash,
|
|
125
|
+
artifactRoot: artifactDir,
|
|
126
|
+
bootstrapNonce,
|
|
127
|
+
elevated: await isWindowsProcessElevated().catch(() => false),
|
|
128
|
+
error: {
|
|
129
|
+
name: error instanceof Error ? error.name : "Error",
|
|
130
|
+
code: typeof protocolError?.code === "string" ? protocolError.code : null,
|
|
131
|
+
message: error instanceof Error ? error.message : String(error),
|
|
132
|
+
stack: error instanceof Error ? error.stack ?? null : null,
|
|
133
|
+
nextAction: typeof protocolError?.nextAction === "string" ? protocolError.nextAction : null
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
await writeFile(temporary, `${JSON.stringify(report, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
137
|
+
await rename(temporary, path);
|
|
138
|
+
}
|
|
139
|
+
let lifecycleSeq = 0;
|
|
140
|
+
let lifecycleLoaded = false;
|
|
141
|
+
let lifecycleTail = Promise.resolve();
|
|
142
|
+
function appendLifecycle(event) {
|
|
143
|
+
const operation = lifecycleTail.then(() => appendLifecycleOwned(event));
|
|
144
|
+
lifecycleTail = operation.catch(() => undefined);
|
|
145
|
+
return operation;
|
|
146
|
+
}
|
|
147
|
+
async function appendLifecycleOwned(event) {
|
|
148
|
+
await mkdir(artifactDir, { recursive: true });
|
|
149
|
+
if (!lifecycleLoaded) {
|
|
150
|
+
lifecycleLoaded = true;
|
|
151
|
+
try {
|
|
152
|
+
const text = await readFile(join(artifactDir, "frida-broker-lifecycle.jsonl"), "utf8");
|
|
153
|
+
for (const line of text.split(/\r?\n/)) {
|
|
154
|
+
if (!line)
|
|
155
|
+
continue;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(line);
|
|
158
|
+
if (Number.isSafeInteger(parsed.seq))
|
|
159
|
+
lifecycleSeq = Math.max(lifecycleSeq, Number(parsed.seq));
|
|
160
|
+
}
|
|
161
|
+
catch { /* preserve malformed history; the process log remains append-only */ }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error.code !== "ENOENT")
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const normalized = normalizeLifecycleEvent(event, ++lifecycleSeq, instanceId);
|
|
170
|
+
const lifecycleFile = join(artifactDir, "frida-broker-lifecycle.jsonl");
|
|
171
|
+
const lifecycleHandle = await open(lifecycleFile, "a", 0o600);
|
|
172
|
+
try {
|
|
173
|
+
await lifecycleHandle.write(JSON.stringify(normalized) + "\n", null, "utf8");
|
|
174
|
+
await lifecycleHandle.sync();
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await lifecycleHandle.close();
|
|
178
|
+
}
|
|
179
|
+
const value = normalized;
|
|
180
|
+
const operation = typeof value.operation === "string" ? value.operation : null;
|
|
181
|
+
const focusOperation = operation?.startsWith("wow_focus_") || operation?.startsWith("wow_watch_") || operation === "wow_session_checkpoint";
|
|
182
|
+
const result = lifecycleResultProjection(value);
|
|
183
|
+
const sessionId = textValue(value.sessionId) ?? textValue(result.sessionId);
|
|
184
|
+
const resourceId = textValue(value.resourceId) ?? textValue(result.resourceId);
|
|
185
|
+
const pid = Number.isSafeInteger(value.pid) ? Number(value.pid) : null;
|
|
186
|
+
await processLog.append({
|
|
187
|
+
buildKey: typeof value.buildKey === "string" ? value.buildKey : null,
|
|
188
|
+
phase: focusOperation ? (operation?.startsWith("wow_watch_") ? "data-root" : "lua-trace") : "verify",
|
|
189
|
+
action: focusOperation ? `${operation}_${String(value.action ?? "lifecycle")}` : `broker_${String(value.action ?? "lifecycle")}`,
|
|
190
|
+
tool: "frida-mcp",
|
|
191
|
+
command: operation,
|
|
192
|
+
status: typeof value.status === "string" ? value.status : "partial",
|
|
193
|
+
target: {
|
|
194
|
+
name: sessionId ?? "Wow.exe",
|
|
195
|
+
kind: sessionId ? "focused_session" : "module",
|
|
196
|
+
pid,
|
|
197
|
+
sessionId: sessionId ?? null,
|
|
198
|
+
resourceId: resourceId ?? null,
|
|
199
|
+
va: null,
|
|
200
|
+
rva: null,
|
|
201
|
+
imageBase: "0x140000000",
|
|
202
|
+
moduleBase: null,
|
|
203
|
+
runtimeAddress: null
|
|
204
|
+
},
|
|
205
|
+
result: {
|
|
206
|
+
...result,
|
|
207
|
+
requestId: textValue(value.requestId) ?? textValue(result.requestId) ?? null,
|
|
208
|
+
clientId: textValue(value.clientId) ?? textValue(result.clientId) ?? null,
|
|
209
|
+
sessionId: sessionId ?? null,
|
|
210
|
+
resourceId: resourceId ?? null,
|
|
211
|
+
pid,
|
|
212
|
+
buildKey: typeof value.buildKey === "string" ? value.buildKey : null
|
|
213
|
+
},
|
|
214
|
+
error: value.error ?? null,
|
|
215
|
+
nextAction: typeof value.nextAction === "string" ? value.nextAction : null
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
export function normalizeLifecycleEvent(event, seq, writerInstanceId, timestamp = new Date().toISOString()) {
|
|
219
|
+
const value = event;
|
|
220
|
+
const result = value.result && typeof value.result === "object" && !Array.isArray(value.result)
|
|
221
|
+
? value.result
|
|
222
|
+
: {};
|
|
223
|
+
const durationCandidate = typeof value.durationMs === "number" ? value.durationMs : value.duration;
|
|
224
|
+
const durationMs = typeof durationCandidate === "number" && Number.isFinite(durationCandidate) && durationCandidate >= 0
|
|
225
|
+
? durationCandidate
|
|
226
|
+
: 0;
|
|
227
|
+
const dropSummary = objectValue(value.dropSummary) ?? objectValue(result.dropSummary) ?? {
|
|
228
|
+
droppedEventCount: nonNegativeNumber(value.droppedEventCount) ?? nonNegativeNumber(result.droppedEventCount) ?? 0,
|
|
229
|
+
droppedByteCount: nonNegativeNumber(value.droppedByteCount) ?? nonNegativeNumber(result.droppedByteCount) ?? 0,
|
|
230
|
+
overflowCount: nonNegativeNumber(value.overflowCount) ?? nonNegativeNumber(result.overflowCount) ?? 0
|
|
231
|
+
};
|
|
232
|
+
const cleanupSummary = objectValue(value.cleanupSummary) ?? objectValue(result.cleanupSummary) ?? {
|
|
233
|
+
cleanupAttempted: booleanValue(value.cleanupAttempted) ?? booleanValue(result.cleanupAttempted) ?? false,
|
|
234
|
+
fullyReleased: booleanValue(value.fullyReleased) ?? booleanValue(result.fullyReleased) ?? null,
|
|
235
|
+
cleanupStatus: objectValue(value.cleanupStatus) ?? objectValue(result.cleanupStatus) ?? null,
|
|
236
|
+
residualResourceIds: arrayValue(value.residualResourceIds) ?? arrayValue(result.residualResourceIds) ?? [],
|
|
237
|
+
residualSessionId: textValue(value.residualSessionId) ?? textValue(result.residualSessionId) ?? null
|
|
238
|
+
};
|
|
239
|
+
const error = Object.hasOwn(value, "error") ? value.error : null;
|
|
240
|
+
return {
|
|
241
|
+
...value,
|
|
242
|
+
timestamp: textValue(value.timestamp) ?? timestamp,
|
|
243
|
+
seq,
|
|
244
|
+
instanceId: writerInstanceId,
|
|
245
|
+
clientId: textValue(value.clientId) ?? textValue(result.clientId) ?? null,
|
|
246
|
+
requestId: textValue(value.requestId) ?? textValue(result.requestId) ?? null,
|
|
247
|
+
resourceId: textValue(value.resourceId) ?? textValue(result.resourceId) ?? null,
|
|
248
|
+
sessionId: textValue(value.sessionId) ?? textValue(result.sessionId) ?? null,
|
|
249
|
+
operation: textValue(value.operation) ?? textValue(value.action) ?? "broker_lifecycle",
|
|
250
|
+
pid: Number.isSafeInteger(value.pid) ? Number(value.pid) : Number.isSafeInteger(result.pid) ? Number(result.pid) : null,
|
|
251
|
+
buildKey: textValue(value.buildKey) ?? textValue(result.buildKey) ?? null,
|
|
252
|
+
duration: durationMs,
|
|
253
|
+
durationMs,
|
|
254
|
+
result: Object.hasOwn(value, "result") ? value.result : null,
|
|
255
|
+
status: textValue(value.status) ?? (error === null || error === undefined ? "partial" : "failed"),
|
|
256
|
+
dropSummary,
|
|
257
|
+
cleanupSummary,
|
|
258
|
+
error: error ?? null,
|
|
259
|
+
nextAction: textValue(value.nextAction) ?? null
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function lifecycleResultProjection(value) {
|
|
263
|
+
const source = value.result && typeof value.result === "object" && !Array.isArray(value.result)
|
|
264
|
+
? value.result
|
|
265
|
+
: {};
|
|
266
|
+
const projected = {};
|
|
267
|
+
for (const key of [
|
|
268
|
+
"requestId", "clientId", "sessionId", "resourceId", "pid", "buildKey",
|
|
269
|
+
"cleanupAttempted", "fullyReleased", "alreadyReleased", "cleanupStatus",
|
|
270
|
+
"residualSessionId", "residualResourceIds", "attachmentDetachStatus",
|
|
271
|
+
"seq", "eventKind", "diff", "reason", "cleanupSteps"
|
|
272
|
+
])
|
|
273
|
+
if (Object.hasOwn(source, key))
|
|
274
|
+
projected[key] = source[key];
|
|
275
|
+
return projected;
|
|
276
|
+
}
|
|
277
|
+
function textValue(value) {
|
|
278
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
279
|
+
}
|
|
280
|
+
function objectValue(value) {
|
|
281
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
282
|
+
}
|
|
283
|
+
function arrayValue(value) {
|
|
284
|
+
return Array.isArray(value) ? value : undefined;
|
|
285
|
+
}
|
|
286
|
+
function booleanValue(value) {
|
|
287
|
+
return typeof value === "boolean" ? value : undefined;
|
|
288
|
+
}
|
|
289
|
+
function nonNegativeNumber(value) {
|
|
290
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
291
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export const BROKER_PROTOCOL_VERSION = "2026-07-28";
|
|
4
|
+
export const BROKER_SCHEMA_VERSION = 1;
|
|
5
|
+
export const BROKER_MAX_FRAME_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
export const BROKER_MAX_INLINE_BYTES = 1024 * 1024;
|
|
7
|
+
export const BROKER_DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
export function canonicalArtifactRoot(value) {
|
|
9
|
+
if (typeof value !== "string" || !value.trim())
|
|
10
|
+
throw new BrokerProtocolError("BROKER_ARTIFACT_ROOT_MISMATCH", "artifactRoot must be a non-empty absolute path");
|
|
11
|
+
return resolve(value);
|
|
12
|
+
}
|
|
13
|
+
export const BROKER_ERROR_CODES = [
|
|
14
|
+
"BROKER_STOPPED", "BOOTSTRAP_LOCK_BUSY", "BOOTSTRAP_TIMEOUT",
|
|
15
|
+
"BROKER_EXECUTABLE_INVALID", "BOOTSTRAP_SPAWN_FAILED", "UAC_CANCELLED",
|
|
16
|
+
"UAC_FAILED", "READY_IDENTITY_MISMATCH", "ACL_MISMATCH", "STALE_LOCK_UNSAFE",
|
|
17
|
+
"FRAME_INVALID", "FRAME_TOO_LARGE", "PROTOCOL_VERSION_MISMATCH", "REQUEST_TIMEOUT",
|
|
18
|
+
"REQUEST_CANCELLED", "REQUEST_NOT_FOUND", "REQUEST_ID_REPLAYED", "PID_REQUIRED",
|
|
19
|
+
"PID_NOT_WOW", "PID_BUILD_MISMATCH", "PID_REUSED", "MODULE_NOT_FOUND",
|
|
20
|
+
"SELECTOR_EMPTY", "SELECTOR_INVALID", "TARGETS_OVER_LIMIT", "RESOURCE_NOT_FOUND",
|
|
21
|
+
"RESOURCE_NOT_OWNER", "RESOURCE_ALREADY_RELEASED", "RESOURCE_REF_UNDERFLOW",
|
|
22
|
+
"SESSION_TARGET_MISMATCH", "CAPTURE_FATAL", "ATTACHMENT_FATAL", "BROKER_FATAL",
|
|
23
|
+
"CLEANUP_TIMEOUT", "ARTIFACT_MANIFEST_CONFLICT", "ARTIFACT_VERIFY_FAILED",
|
|
24
|
+
"MUTATION_PLAN_REQUIRED", "MUTATION_CONFIRMATION_REQUIRED", "MUTATION_EXPECTED_EFFECT_REQUIRED",
|
|
25
|
+
"MUTATION_ROLLBACK_REQUIRED", "MUTATION_ROLLBACK_INVALID", "MUTATION_IRREVERSIBLE_CONFIRMATION_REQUIRED",
|
|
26
|
+
"MUTATION_EVIDENCE_PLAN_REQUIRED", "MUTATION_AUDIT_REQUIRED", "MUTATION_CONFIRMATION_EXPIRED",
|
|
27
|
+
"MUTATION_CONFIRMATION_REPLAYED", "MUTATION_TARGET_MISMATCH", "MUTATION_EVIDENCE_MISMATCH",
|
|
28
|
+
"MUTATION_BEFORE_EVIDENCE_FAILED", "MUTATION_EXECUTION_FAILED", "MUTATION_AFTER_EVIDENCE_FAILED",
|
|
29
|
+
"MUTATION_ROLLBACK_UNAVAILABLE", "MUTATION_ROLLBACK_FAILED", "MUTATION_OUTCOME_UNKNOWN",
|
|
30
|
+
"SPAWN_CONTRACT_REQUIRED", "SPAWN_PID_MUST_BE_NULL", "SPAWN_BUILD_MISMATCH",
|
|
31
|
+
"HOST_CALL_BUILD_KEY_REQUIRED", "HOST_CALL_NOT_ALLOWLISTED", "HOST_CALL_SCHEMA_MISMATCH",
|
|
32
|
+
"BROKER_ARTIFACT_ROOT_MISMATCH"
|
|
33
|
+
];
|
|
34
|
+
export class BrokerProtocolError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
nextAction;
|
|
37
|
+
constructor(code, message, nextAction = "Inspect Broker status and retry with a valid request.") {
|
|
38
|
+
super(message);
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.nextAction = nextAction;
|
|
41
|
+
this.name = "BrokerProtocolError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function sidHash(sid) {
|
|
45
|
+
return createHash("sha256").update(sid, "utf8").digest("hex").slice(0, 24);
|
|
46
|
+
}
|
|
47
|
+
export function pipeNameForSid(sid) {
|
|
48
|
+
return `\\\\.\\pipe\\wowdump-frida-${sidHash(sid)}`;
|
|
49
|
+
}
|
|
50
|
+
export function canonicalPayloadHash(request) {
|
|
51
|
+
return createHash("sha256").update(stableJson({
|
|
52
|
+
clientId: request.clientId,
|
|
53
|
+
operation: request.operation,
|
|
54
|
+
pid: request.pid,
|
|
55
|
+
buildKey: request.buildKey,
|
|
56
|
+
payload: request.payload
|
|
57
|
+
})).digest("hex");
|
|
58
|
+
}
|
|
59
|
+
export function validateRequest(value) {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
fail("FRAME_INVALID", "request must be an object");
|
|
62
|
+
const request = value;
|
|
63
|
+
if (request.schemaVersion !== BROKER_SCHEMA_VERSION || request.protocolVersion !== BROKER_PROTOCOL_VERSION) {
|
|
64
|
+
fail("PROTOCOL_VERSION_MISMATCH", "unsupported Broker schema or protocol version");
|
|
65
|
+
}
|
|
66
|
+
for (const key of ["requestId", "clientId", "operation"]) {
|
|
67
|
+
if (typeof request[key] !== "string" || !request[key].trim())
|
|
68
|
+
fail("FRAME_INVALID", `${key} must be a non-empty string`);
|
|
69
|
+
}
|
|
70
|
+
if (request.pid !== null && (!Number.isSafeInteger(request.pid) || request.pid <= 0))
|
|
71
|
+
fail("FRAME_INVALID", "pid must be null or a positive integer");
|
|
72
|
+
if (request.buildKey !== null && typeof request.buildKey !== "string")
|
|
73
|
+
fail("FRAME_INVALID", "buildKey must be null or a string");
|
|
74
|
+
if (!request.payload || typeof request.payload !== "object" || Array.isArray(request.payload))
|
|
75
|
+
fail("FRAME_INVALID", "payload must be an object");
|
|
76
|
+
return request;
|
|
77
|
+
}
|
|
78
|
+
export function encodeFrame(value) {
|
|
79
|
+
const body = Buffer.from(JSON.stringify(value), "utf8");
|
|
80
|
+
if (body.length > BROKER_MAX_FRAME_BYTES)
|
|
81
|
+
fail("FRAME_TOO_LARGE", `frame exceeds ${BROKER_MAX_FRAME_BYTES} bytes`);
|
|
82
|
+
const frame = Buffer.allocUnsafe(body.length + 4);
|
|
83
|
+
frame.writeUInt32LE(body.length, 0);
|
|
84
|
+
body.copy(frame, 4);
|
|
85
|
+
return frame;
|
|
86
|
+
}
|
|
87
|
+
export class FrameDecoder {
|
|
88
|
+
buffered = Buffer.alloc(0);
|
|
89
|
+
push(chunk) {
|
|
90
|
+
this.buffered = Buffer.concat([this.buffered, chunk]);
|
|
91
|
+
const values = [];
|
|
92
|
+
while (this.buffered.length >= 4) {
|
|
93
|
+
const length = this.buffered.readUInt32LE(0);
|
|
94
|
+
if (length > BROKER_MAX_FRAME_BYTES)
|
|
95
|
+
fail("FRAME_TOO_LARGE", `frame exceeds ${BROKER_MAX_FRAME_BYTES} bytes`);
|
|
96
|
+
if (this.buffered.length < length + 4)
|
|
97
|
+
break;
|
|
98
|
+
const body = this.buffered.subarray(4, length + 4);
|
|
99
|
+
this.buffered = this.buffered.subarray(length + 4);
|
|
100
|
+
try {
|
|
101
|
+
values.push(JSON.parse(body.toString("utf8")));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
fail("FRAME_INVALID", "frame body is not valid JSON");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return values;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function stableJson(value) {
|
|
111
|
+
if (Array.isArray(value))
|
|
112
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
113
|
+
if (value && typeof value === "object")
|
|
114
|
+
return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`).join(",")}}`;
|
|
115
|
+
return JSON.stringify(value);
|
|
116
|
+
}
|
|
117
|
+
function fail(code, message) {
|
|
118
|
+
throw new BrokerProtocolError(code, message);
|
|
119
|
+
}
|