Files
openclaw/src/gateway/http-common.ts
2026-02-23 19:47:29 +00:00

108 lines
3.3 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import type { GatewayAuthResult } from "./auth.js";
import { readJsonBody } from "./hooks.js";
/**
* Apply baseline security headers that are safe for all response types (API JSON,
* HTML pages, static assets, SSE streams). Headers that restrict framing or set a
* Content-Security-Policy are intentionally omitted here because some handlers
* (canvas host, A2UI) serve content that may be loaded inside frames.
*/
export function setDefaultSecurityHeaders(
res: ServerResponse,
opts?: { strictTransportSecurity?: string },
) {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "no-referrer");
const strictTransportSecurity = opts?.strictTransportSecurity;
if (typeof strictTransportSecurity === "string" && strictTransportSecurity.length > 0) {
res.setHeader("Strict-Transport-Security", strictTransportSecurity);
}
}
export function sendJson(res: ServerResponse, status: number, body: unknown) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(body));
}
export function sendText(res: ServerResponse, status: number, body: string) {
res.statusCode = status;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(body);
}
export function sendMethodNotAllowed(res: ServerResponse, allow = "POST") {
res.setHeader("Allow", allow);
sendText(res, 405, "Method Not Allowed");
}
export function sendUnauthorized(res: ServerResponse) {
sendJson(res, 401, {
error: { message: "Unauthorized", type: "unauthorized" },
});
}
export function sendRateLimited(res: ServerResponse, retryAfterMs?: number) {
if (retryAfterMs && retryAfterMs > 0) {
res.setHeader("Retry-After", String(Math.ceil(retryAfterMs / 1000)));
}
sendJson(res, 429, {
error: {
message: "Too many failed authentication attempts. Please try again later.",
type: "rate_limited",
},
});
}
export function sendGatewayAuthFailure(res: ServerResponse, authResult: GatewayAuthResult) {
if (authResult.rateLimited) {
sendRateLimited(res, authResult.retryAfterMs);
return;
}
sendUnauthorized(res);
}
export function sendInvalidRequest(res: ServerResponse, message: string) {
sendJson(res, 400, {
error: { message, type: "invalid_request_error" },
});
}
export async function readJsonBodyOrError(
req: IncomingMessage,
res: ServerResponse,
maxBytes: number,
): Promise<unknown> {
const body = await readJsonBody(req, maxBytes);
if (!body.ok) {
if (body.error === "payload too large") {
sendJson(res, 413, {
error: { message: "Payload too large", type: "invalid_request_error" },
});
return undefined;
}
if (body.error === "request body timeout") {
sendJson(res, 408, {
error: { message: "Request body timeout", type: "invalid_request_error" },
});
return undefined;
}
sendInvalidRequest(res, body.error);
return undefined;
}
return body.value;
}
export function writeDone(res: ServerResponse) {
res.write("data: [DONE]\n\n");
}
export function setSseHeaders(res: ServerResponse) {
res.statusCode = 200;
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders?.();
}