mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 19:26:12 +00:00
fix(tlon): bound urbit scry JSON response reads (#100376)
* fix(tlon): bound urbit scry JSON response reads * fix(tlon): preserve bounded scry diagnostics * refactor(tlon): tighten scry boundary proof * docs(changelog): stabilize Tlon response entry * docs(changelog): close out maintainer batch * chore(changelog): defer batch closeout --------- Co-authored-by: NIO <nocodet@mail.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ describe("Urbit channel operations", () => {
|
||||
vi.mocked(urbitFetch).mockReset();
|
||||
});
|
||||
|
||||
it("wraps malformed scry response JSON", async () => {
|
||||
it("rejects malformed scry response JSON", async () => {
|
||||
const release = vi.fn().mockResolvedValue(undefined);
|
||||
vi.mocked(urbitFetch).mockResolvedValue({
|
||||
response: new Response("{not json", {
|
||||
@@ -31,7 +31,9 @@ describe("Urbit channel operations", () => {
|
||||
},
|
||||
{ path: "/chat/inbox.json", auditContext: "test" },
|
||||
),
|
||||
).rejects.toThrow("Urbit scry response was malformed JSON for path /chat/inbox.json");
|
||||
).rejects.toThrow(
|
||||
"Tlon scry response for path /chat/inbox.json: malformed JSON response",
|
||||
);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Tlon plugin module implements channel ops behavior.
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import type { LookupFn, SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { UrbitHttpError } from "./errors.js";
|
||||
import { urbitFetch } from "./fetch.js";
|
||||
@@ -60,7 +63,9 @@ export async function pokeUrbitChannel(
|
||||
|
||||
try {
|
||||
if (!response.ok && response.status !== 204) {
|
||||
const errorText = await readResponseTextLimited(response, TLON_ERROR_BODY_LIMIT_BYTES).catch(() => "");
|
||||
const errorText = await readResponseTextLimited(response, TLON_ERROR_BODY_LIMIT_BYTES).catch(
|
||||
() => "",
|
||||
);
|
||||
throw new Error(`Poke failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`);
|
||||
}
|
||||
return pokeId;
|
||||
@@ -92,11 +97,9 @@ export async function scryUrbitPath(
|
||||
if (!response.ok) {
|
||||
throw new Error(`Scry failed: ${response.status} for path ${params.path}`);
|
||||
}
|
||||
try {
|
||||
return await response.json();
|
||||
} catch (cause) {
|
||||
throw new Error(`Urbit scry response was malformed JSON for path ${params.path}`, { cause });
|
||||
}
|
||||
// Successful scry bodies come from a remote Urbit and have no protocol size bound.
|
||||
// Keep the shared JSON ceiling while retaining the path needed to identify the endpoint.
|
||||
return await readProviderJsonResponse(response, `Tlon scry response for path ${params.path}`);
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
|
||||
@@ -19,9 +19,18 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const { pokeUrbitChannel } = await import("./channel-ops.js");
|
||||
const { pokeUrbitChannel, scryUrbitPath } = await import("./channel-ops.js");
|
||||
|
||||
const CHUNK = Buffer.alloc(64 * 1024, "X");
|
||||
const SCRY_PATH = "/groups-ui/v6/init.json";
|
||||
|
||||
async function listen(server: http.Server): Promise<number> {
|
||||
return await new Promise<number>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
resolve((server.address() as { port: number }).port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("tlon error body boundary", () => {
|
||||
let server: http.Server;
|
||||
@@ -52,11 +61,7 @@ describe("tlon error body boundary", () => {
|
||||
}
|
||||
write();
|
||||
});
|
||||
const port = await new Promise<number>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
resolve((server.address() as { port: number }).port);
|
||||
});
|
||||
});
|
||||
const port = await listen(server);
|
||||
|
||||
const err = await pokeUrbitChannel(
|
||||
{
|
||||
@@ -79,11 +84,7 @@ describe("tlon error body boundary", () => {
|
||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||
res.end("session expired");
|
||||
});
|
||||
const port = await new Promise<number>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
resolve((server.address() as { port: number }).port);
|
||||
});
|
||||
});
|
||||
const port = await listen(server);
|
||||
|
||||
const err = await pokeUrbitChannel(
|
||||
{
|
||||
@@ -98,4 +99,54 @@ describe("tlon error body boundary", () => {
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect((err as Error).message).toContain("session expired");
|
||||
});
|
||||
|
||||
it("parses a normal scry response over HTTP", async () => {
|
||||
server = http.createServer((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ groups: {} }));
|
||||
});
|
||||
const port = await listen(server);
|
||||
|
||||
await expect(
|
||||
scryUrbitPath(
|
||||
{ baseUrl: `http://127.0.0.1:${port}`, cookie: "urbit=cookie" },
|
||||
{ path: SCRY_PATH, auditContext: "test" },
|
||||
),
|
||||
).resolves.toEqual({ groups: {} });
|
||||
});
|
||||
|
||||
it("bounds a streaming successful scry response over HTTP", async () => {
|
||||
server = http.createServer((_req, res) => {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.write('{"payload":"');
|
||||
let written = 0;
|
||||
const write = () => {
|
||||
if (res.destroyed) {
|
||||
return;
|
||||
}
|
||||
if (written >= 18 * 1024 * 1024) {
|
||||
res.end('"}');
|
||||
return;
|
||||
}
|
||||
written += CHUNK.length;
|
||||
if (res.write(CHUNK)) {
|
||||
setImmediate(write);
|
||||
} else {
|
||||
res.once("drain", write);
|
||||
}
|
||||
};
|
||||
write();
|
||||
});
|
||||
const port = await listen(server);
|
||||
|
||||
const error = await scryUrbitPath(
|
||||
{ baseUrl: `http://127.0.0.1:${port}`, cookie: "urbit=cookie" },
|
||||
{ path: SCRY_PATH, auditContext: "test" },
|
||||
).catch((cause: unknown) => cause);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe(
|
||||
`Tlon scry response for path ${SCRY_PATH}: JSON response exceeds 16777216 bytes`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user