mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
49b4edfe77
Share padded decoding across CDP response shapes and cache fingerprints, and count binary WebSocket bytes without allocating a decoded buffer. Preserve HAR canonical pad bits and validate before allocation. Replace repeated alphabet quartets in canonical schemas with separate alphabet, padding and fixed-width length assertions. Cover near-32MiB encodings under ordinary, Unicode and Unicode-set modes, plus complete 4MiB CLI and MCP retained-evidence export workflows. Fixes #5253
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
|
|
import { CdpCaptureEvents } from "./CdpCaptureEvents.js";
|
|
import { captureStorageFingerprints } from "./CdpCaptureStorageFingerprints.js";
|
|
import type { CdpConnection } from "./CdpConnection.js";
|
|
|
|
const ORIGIN = "https://example.test";
|
|
const CDP_ENDPOINT = "http://127.0.0.1:9222";
|
|
const BODY_BYTES = 4 * 1024 * 1024;
|
|
|
|
const captureCacheFingerprint = async (encodedBody: string) => {
|
|
const send = vi.fn(async (method: string) => {
|
|
if (method === "Network.getCookies") return { cookies: [] };
|
|
if (method === "CacheStorage.requestEntries")
|
|
return {
|
|
cacheDataEntries: [
|
|
{
|
|
requestURL: `${ORIGIN}/cached`,
|
|
requestMethod: "GET",
|
|
requestHeaders: [],
|
|
},
|
|
],
|
|
returnCount: 1,
|
|
};
|
|
if (method === "CacheStorage.requestCachedResponse")
|
|
return { response: { body: encodedBody } };
|
|
throw new Error(`Unexpected CDP command ${method}`);
|
|
});
|
|
const input = inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: CDP_ENDPOINT,
|
|
allowed_origins: [ORIGIN],
|
|
target_id: "page",
|
|
});
|
|
return captureStorageFingerprints({
|
|
context: {
|
|
connection: { send } as unknown as CdpConnection,
|
|
sessionId: "page-session",
|
|
input,
|
|
},
|
|
origin: ORIGIN,
|
|
cookieUrl: `${ORIGIN}/`,
|
|
local: { items: [], complete: true },
|
|
session: { items: [], complete: true },
|
|
indexedDbNames: [],
|
|
caches: [{ name: "cache", id: "cache-id" }],
|
|
limitations: [],
|
|
});
|
|
};
|
|
|
|
const createEvents = (includeJsonBodyShapes = true): CdpCaptureEvents =>
|
|
new CdpCaptureEvents(
|
|
inspectWebPageInputSchema.parse({
|
|
cdp_endpoint: CDP_ENDPOINT,
|
|
allowed_origins: [ORIGIN],
|
|
target_id: "page",
|
|
include_json_body_shapes: includeJsonBodyShapes,
|
|
}),
|
|
new Set([ORIGIN]),
|
|
);
|
|
|
|
const startJsonResponse = (
|
|
events: CdpCaptureEvents,
|
|
requestId: string,
|
|
path: string,
|
|
): void => {
|
|
events.ingest({
|
|
method: "Network.requestWillBeSent",
|
|
params: {
|
|
requestId,
|
|
request: { url: `${ORIGIN}${path}`, method: "GET" },
|
|
initiator: { type: "other" },
|
|
timestamp: 1,
|
|
},
|
|
});
|
|
events.ingest({
|
|
method: "Network.responseReceived",
|
|
params: {
|
|
requestId,
|
|
response: {
|
|
url: `${ORIGIN}${path}`,
|
|
status: 200,
|
|
mimeType: "application/json",
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
it("fingerprints large cached bodies and retains CDP padding-bit acceptance", async () => {
|
|
const body = Buffer.alloc(BODY_BYTES, 0x81);
|
|
const large = await captureCacheFingerprint(body.toString("base64"));
|
|
expect(large.complete).toBe(true);
|
|
expect(large.items[0]?.value_sha256).toBe(
|
|
createHash("sha256").update(body).digest("hex"),
|
|
);
|
|
|
|
const padBits = await captureCacheFingerprint("e31=");
|
|
expect(padBits.items[0]?.value_sha256).toBe(
|
|
createHash("sha256").update(Buffer.from("e31=", "base64")).digest("hex"),
|
|
);
|
|
|
|
const malformed = await captureCacheFingerprint("%%%not-base64%%%");
|
|
expect(malformed.complete).toBe(false);
|
|
expect(malformed.items[0]?.value_sha256).toBeNull();
|
|
});
|
|
|
|
it("decodes large JSON response bodies and preserves malformed and pad-bit behavior", () => {
|
|
const events = createEvents();
|
|
startJsonResponse(events, "large-json", "/large.json");
|
|
const encoded = Buffer.from(
|
|
JSON.stringify({ value: "x".repeat(BODY_BYTES) }),
|
|
).toString("base64");
|
|
const candidate = events.responseBodyRequests()[0];
|
|
expect(candidate).toBeDefined();
|
|
events.ingestResponseBody(candidate!, {
|
|
body: encoded,
|
|
base64Encoded: true,
|
|
});
|
|
expect(events.network.get("large-json")?.body_shapes.response).toMatchObject({
|
|
root_type: "object",
|
|
node_count: 2,
|
|
});
|
|
|
|
startJsonResponse(events, "pad-bits", "/pad-bits.json");
|
|
const padBits = events
|
|
.responseBodyRequests()
|
|
.find(({ requestId }) => requestId === "pad-bits");
|
|
expect(padBits).toBeDefined();
|
|
events.ingestResponseBody(padBits!, {
|
|
body: "e31=",
|
|
base64Encoded: true,
|
|
});
|
|
expect(events.network.get("pad-bits")?.body_shapes.response?.root_type).toBe(
|
|
"object",
|
|
);
|
|
|
|
startJsonResponse(events, "malformed", "/malformed.json");
|
|
const malformed = events
|
|
.responseBodyRequests()
|
|
.find(({ requestId }) => requestId === "malformed");
|
|
expect(malformed).toBeDefined();
|
|
events.ingestResponseBody(malformed!, {
|
|
body: "%%%not-base64%%%",
|
|
base64Encoded: true,
|
|
});
|
|
expect(events.network.get("malformed")?.body_shapes.response).toBeNull();
|
|
});
|
|
|
|
it("counts large binary WebSocket frames without accepting malformed base64", () => {
|
|
const events = createEvents(false);
|
|
const encoded = Buffer.alloc(BODY_BYTES, 0x81).toString("base64");
|
|
events.ingest({
|
|
method: "Network.webSocketCreated",
|
|
params: { requestId: "socket", url: "wss://example.test/socket" },
|
|
});
|
|
events.ingest({
|
|
method: "Network.webSocketFrameReceived",
|
|
params: {
|
|
requestId: "socket",
|
|
response: { opcode: 2, payloadData: encoded },
|
|
},
|
|
});
|
|
expect(
|
|
events.webSocketConnectionsById.get("socket")?.events[0]?.payload_bytes,
|
|
).toBe(BODY_BYTES);
|
|
|
|
events.ingest({
|
|
method: "Network.webSocketFrameReceived",
|
|
params: {
|
|
requestId: "socket",
|
|
response: { opcode: 2, payloadData: "%%%not-base64%%%" },
|
|
},
|
|
});
|
|
expect(
|
|
events.webSocketConnectionsById.get("socket")?.events[1]?.payload_bytes,
|
|
).toBeNull();
|
|
expect(events.completeness.snapshot().excluded).toContainEqual({
|
|
section: "websocket_frames",
|
|
reason: "invalid_protocol_value",
|
|
count: 1,
|
|
});
|
|
});
|