mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(gateway): preserve sandbox HEAD response metadata (#118553)
Punchcard-Session: coral-summit-orchard-93
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMcpAppSandboxPath } from "../agents/mcp-app-sandbox.js";
|
||||
import { createSandboxHostHttpServer } from "./mcp-app-sandbox-http.js";
|
||||
@@ -12,6 +13,25 @@ function request(url: string, method: "GET" | "HEAD" | "POST" = "GET") {
|
||||
return { res, end, setHeader };
|
||||
}
|
||||
|
||||
async function withSandboxHost(run: (origin: string) => Promise<void>): Promise<void> {
|
||||
const server = createSandboxHostHttpServer();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const address = server.address() as AddressInfo;
|
||||
try {
|
||||
await run(`http://127.0.0.1:${address.port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe("MCP App sandbox HTTP origin", () => {
|
||||
it("serves only the proxy endpoint with metadata-derived CSP", () => {
|
||||
const result = request(
|
||||
@@ -80,4 +100,50 @@ describe("MCP App sandbox HTTP origin", () => {
|
||||
expect.stringContaining("connect-src https://xn--bcher-kva.example"),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "sandbox HTML",
|
||||
path: buildMcpAppSandboxPath(),
|
||||
statusCode: 200,
|
||||
},
|
||||
{
|
||||
label: "missing path",
|
||||
path: "/missing",
|
||||
statusCode: 404,
|
||||
},
|
||||
{
|
||||
label: "malformed policy",
|
||||
path: `${buildMcpAppSandboxPath()}?csp=not-json`,
|
||||
statusCode: 400,
|
||||
},
|
||||
])(
|
||||
"keeps GET and HEAD representation metadata aligned for $label",
|
||||
async ({ path, statusCode }) => {
|
||||
await withSandboxHost(async (origin) => {
|
||||
const get = await fetch(`${origin}${path}`);
|
||||
const head = await fetch(`${origin}${path}`, { method: "HEAD" });
|
||||
const getBody = await get.text();
|
||||
|
||||
expect(get.status).toBe(statusCode);
|
||||
expect(head.status).toBe(statusCode);
|
||||
expect(getBody).not.toBe("");
|
||||
expect(await head.text()).toBe("");
|
||||
expect(get.headers.get("content-length")).toBe(String(Buffer.byteLength(getBody)));
|
||||
for (const header of [
|
||||
"content-type",
|
||||
"content-length",
|
||||
"cache-control",
|
||||
"content-security-policy",
|
||||
"permissions-policy",
|
||||
"cross-origin-resource-policy",
|
||||
"origin-agent-cluster",
|
||||
"referrer-policy",
|
||||
"x-content-type-options",
|
||||
]) {
|
||||
expect(head.headers.get(header), header).toBe(get.headers.get(header));
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
decodeSandboxHostCsp,
|
||||
SANDBOX_HOST_PATH,
|
||||
} from "../agents/sandbox-host.js";
|
||||
import { respondPlainText } from "./control-ui-http-utils.js";
|
||||
|
||||
const MCP_APP_PERMISSIONS_POLICY = "camera=(), microphone=(), geolocation=(), clipboard-write=()";
|
||||
|
||||
@@ -20,13 +21,11 @@ function handleMcpAppSandboxHttpRequest(req: IncomingMessage, res: ServerRespons
|
||||
try {
|
||||
url = new URL(req.url ?? "/", "http://localhost");
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.end("Bad Request");
|
||||
respondPlainText(res, 400, "Bad Request");
|
||||
return;
|
||||
}
|
||||
if (url.pathname !== SANDBOX_HOST_PATH || (req.method !== "GET" && req.method !== "HEAD")) {
|
||||
res.statusCode = 404;
|
||||
res.end("Not Found");
|
||||
respondPlainText(res, 404, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,9 +33,7 @@ function handleMcpAppSandboxHttpRequest(req: IncomingMessage, res: ServerRespons
|
||||
try {
|
||||
csp = decodeSandboxHostCsp(url.searchParams.get("csp"));
|
||||
} catch {
|
||||
res.statusCode = 400;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
res.end("invalid MCP App sandbox policy");
|
||||
respondPlainText(res, 400, "invalid MCP App sandbox policy");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,7 +46,10 @@ function handleMcpAppSandboxHttpRequest(req: IncomingMessage, res: ServerRespons
|
||||
res.setHeader("Origin-Agent-Cluster", "?1");
|
||||
res.setHeader("Referrer-Policy", "no-referrer");
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
res.end(req.method === "HEAD" ? undefined : buildSandboxHostProxyHtml(csp));
|
||||
const html = buildSandboxHostProxyHtml(csp);
|
||||
// Keep GET and HEAD representation metadata aligned while suppressing the HEAD body.
|
||||
res.setHeader("Content-Length", String(Buffer.byteLength(html)));
|
||||
res.end(req.method === "HEAD" ? undefined : html);
|
||||
}
|
||||
|
||||
/** Dedicated listener: this origin must never serve Control UI or authenticated Gateway data. */
|
||||
|
||||
Reference in New Issue
Block a user