

























@@ -4,8 +4,12 @@ import {
44type IncomingMessage,
55type ServerResponse,
66} from "node:http";
7+import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
78import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
8910+const DEFAULT_FAULT_PROXY_REQUEST_MAX_BYTES = 16 * 1024 * 1024;
11+const DEFAULT_FAULT_PROXY_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
12+913const HOP_BY_HOP_HEADERS = new Set([
1014"connection",
1115"content-length",
@@ -38,6 +42,17 @@ type MatrixQaFaultProxyForwardedResponse = {
3842status: number;
3943};
404445+class MatrixQaFaultProxyHttpError extends Error {
46+constructor(
47+readonly status: number,
48+readonly code: string,
49+message: string,
50+) {
51+super(message);
52+this.name = "MatrixQaFaultProxyHttpError";
53+}
54+}
55+4156export type MatrixQaFaultProxyRule = {
4257id: string;
4358match(request: MatrixQaFaultProxyRequest): boolean;
@@ -87,12 +102,122 @@ function buildFetchHeaders(headers: IncomingHttpHeaders) {
87102return result;
88103}
8910490-async function readRequestBody(req: IncomingMessage) {
91-const chunks: Buffer[] = [];
92-for await (const chunk of req) {
93-chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
105+function normalizeByteChunk(chunk: string | Buffer): Buffer {
106+return typeof chunk === "string" ? Buffer.from(chunk) : chunk;
107+}
108+109+function rejectOversizedRequestBody(maxBytes: number, size: number) {
110+return new MatrixQaFaultProxyHttpError(
111+413,
112+"MATRIX_QA_FAULT_PROXY_REQUEST_TOO_LARGE",
113+`Matrix QA fault proxy request body exceeds ${maxBytes} bytes (got at least ${size})`,
114+);
115+}
116+117+function rejectAbortedRequestBody() {
118+return new MatrixQaFaultProxyHttpError(
119+400,
120+"MATRIX_QA_FAULT_PROXY_REQUEST_ABORTED",
121+"Matrix QA fault proxy request body ended before upload completed",
122+);
123+}
124+125+function drainRejectedRequestBody(req: IncomingMessage) {
126+const onError = () => undefined;
127+const onClose = () => {
128+req.off("error", onError);
129+};
130+req.on("error", onError);
131+req.once("close", onClose);
132+req.resume();
133+}
134+135+async function readRequestBody(req: IncomingMessage, maxBytes: number) {
136+const contentLength = normalizeHeaderValue(req.headers["content-length"]);
137+if (contentLength !== undefined) {
138+const size = Number(contentLength);
139+if (Number.isFinite(size) && size > maxBytes) {
140+drainRejectedRequestBody(req);
141+throw rejectOversizedRequestBody(maxBytes, size);
142+}
94143}
95-return Buffer.concat(chunks);
144+145+return await new Promise<Buffer>((resolve, reject) => {
146+const chunks: Buffer[] = [];
147+let total = 0;
148+let settled = false;
149+150+const cleanup = () => {
151+req.off("data", onData);
152+req.off("end", onEnd);
153+req.off("error", onError);
154+req.off("aborted", onAborted);
155+req.off("close", onClose);
156+};
157+const stopReading = () => {
158+req.off("data", onData);
159+req.off("end", onEnd);
160+req.off("aborted", onAborted);
161+};
162+const settleReject = (error: Error, options?: { drain?: boolean }) => {
163+if (settled) {
164+return;
165+}
166+settled = true;
167+if (options?.drain) {
168+stopReading();
169+req.resume();
170+} else {
171+cleanup();
172+}
173+reject(error);
174+};
175+const onData = (chunk: string | Buffer) => {
176+const buffer = normalizeByteChunk(chunk);
177+const nextTotal = total + buffer.byteLength;
178+if (nextTotal > maxBytes) {
179+settleReject(rejectOversizedRequestBody(maxBytes, nextTotal), { drain: true });
180+return;
181+}
182+chunks.push(buffer);
183+total = nextTotal;
184+};
185+const onEnd = () => {
186+if (settled) {
187+return;
188+}
189+settled = true;
190+cleanup();
191+resolve(Buffer.concat(chunks, total));
192+};
193+const onError = (error: Error) => {
194+if (settled) {
195+cleanup();
196+return;
197+}
198+settleReject(error);
199+};
200+const onAborted = () => {
201+settleReject(rejectAbortedRequestBody());
202+};
203+const onClose = () => {
204+if (settled) {
205+cleanup();
206+return;
207+}
208+if (!req.complete) {
209+settleReject(rejectAbortedRequestBody());
210+return;
211+}
212+cleanup();
213+};
214+215+req.on("data", onData);
216+req.once("end", onEnd);
217+req.once("error", onError);
218+req.once("aborted", onAborted);
219+req.once("close", onClose);
220+});
96221}
9722298223function bufferToArrayBuffer(buffer: Buffer) {
@@ -113,6 +238,7 @@ function writeJsonResponse(res: ServerResponse, response: MatrixQaFaultProxyResp
113238114239async function forwardMatrixQaFaultProxyRequest(params: {
115240body: Buffer;
241+maxResponseBytes: number;
116242req: IncomingMessage;
117243targetUrl: URL;
118244}): Promise<MatrixQaFaultProxyForwardedResponse> {
@@ -133,7 +259,14 @@ async function forwardMatrixQaFaultProxyRequest(params: {
133259});
134260try {
135261return {
136-body: Buffer.from(await response.arrayBuffer()),
262+body: await readResponseWithLimit(response, params.maxResponseBytes, {
263+onOverflow: ({ size }) =>
264+new MatrixQaFaultProxyHttpError(
265+502,
266+"MATRIX_QA_FAULT_PROXY_RESPONSE_TOO_LARGE",
267+`Matrix QA fault proxy upstream response exceeds ${params.maxResponseBytes} bytes (got at least ${size})`,
268+),
269+}),
137270headers: response.headers,
138271status: response.status,
139272};
@@ -157,10 +290,14 @@ function writeForwardedResponse(
157290}
158291159292export async function startMatrixQaFaultProxy(params: {
293+maxRequestBytes?: number;
294+maxResponseBytes?: number;
160295rules: MatrixQaFaultProxyRule[];
161296targetBaseUrl: string;
162297}): Promise<MatrixQaFaultProxy> {
163298const targetBaseUrl = new URL(params.targetBaseUrl);
299+const maxRequestBytes = params.maxRequestBytes ?? DEFAULT_FAULT_PROXY_REQUEST_MAX_BYTES;
300+const maxResponseBytes = params.maxResponseBytes ?? DEFAULT_FAULT_PROXY_RESPONSE_MAX_BYTES;
164301const hits: MatrixQaFaultProxyHit[] = [];
165302const server = createServer(async (req, res) => {
166303try {
@@ -174,7 +311,7 @@ export async function startMatrixQaFaultProxy(params: {
174311 path,
175312search: requestUrl.search,
176313};
177-const body = await readRequestBody(req);
314+const body = await readRequestBody(req, maxRequestBytes);
178315const rule = params.rules.find((candidate) => candidate.match(request));
179316if (rule) {
180317hits.push({
@@ -189,6 +326,7 @@ export async function startMatrixQaFaultProxy(params: {
189326}
190327const forwarded = await forwardMatrixQaFaultProxyRequest({
191328 body,
329+ maxResponseBytes,
192330 req,
193331targetUrl: requestUrl,
194332});
@@ -201,6 +339,17 @@ export async function startMatrixQaFaultProxy(params: {
201339 : forwarded;
202340writeForwardedResponse(res, response);
203341} catch (error) {
342+if (error instanceof MatrixQaFaultProxyHttpError) {
343+writeJsonResponse(res, {
344+body: {
345+errcode: error.code,
346+error: error.message,
347+},
348+ ...(error.status === 413 ? { headers: { connection: "close" } } : {}),
349+status: error.status,
350+});
351+return;
352+}
204353writeJsonResponse(res, {
205354body: {
206355errcode: "MATRIX_QA_FAULT_PROXY_ERROR",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。