mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 18:24:35 +00:00
fix(synology-chat): bound webhook body read time
This commit is contained in:
committed by
Peter Steinberger
parent
fbd1210ec2
commit
6df36a8b35
@@ -1,3 +1,5 @@
|
|||||||
|
import { EventEmitter } from "node:events";
|
||||||
|
import type { IncomingMessage } from "node:http";
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { makeFormBody, makeReq, makeRes } from "./test-http-utils.js";
|
import { makeFormBody, makeReq, makeRes } from "./test-http-utils.js";
|
||||||
import type { ResolvedSynologyChatAccount } from "./types.js";
|
import type { ResolvedSynologyChatAccount } from "./types.js";
|
||||||
@@ -30,6 +32,24 @@ function makeAccount(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeStalledReq(method: string): IncomingMessage {
|
||||||
|
const req = new EventEmitter() as IncomingMessage & {
|
||||||
|
destroyed: boolean;
|
||||||
|
destroy: () => void;
|
||||||
|
};
|
||||||
|
req.method = method;
|
||||||
|
req.headers = {};
|
||||||
|
req.socket = { remoteAddress: "127.0.0.1" } as any;
|
||||||
|
req.destroyed = false;
|
||||||
|
req.destroy = () => {
|
||||||
|
if (req.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req.destroyed = true;
|
||||||
|
};
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
const validBody = makeFormBody({
|
const validBody = makeFormBody({
|
||||||
token: "valid-token",
|
token: "valid-token",
|
||||||
user_id: "123",
|
user_id: "123",
|
||||||
@@ -95,6 +115,29 @@ describe("createWebhookHandler", () => {
|
|||||||
expect(res._status).toBe(400);
|
expect(res._status).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 408 when request body times out", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
try {
|
||||||
|
const handler = createWebhookHandler({
|
||||||
|
account: makeAccount(),
|
||||||
|
deliver: vi.fn(),
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = makeStalledReq("POST");
|
||||||
|
const res = makeRes();
|
||||||
|
const run = handler(req, res);
|
||||||
|
|
||||||
|
await vi.advanceTimersByTimeAsync(30_000);
|
||||||
|
await run;
|
||||||
|
|
||||||
|
expect(res._status).toBe(408);
|
||||||
|
expect(res._body).toContain("timeout");
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("returns 401 for invalid token", async () => {
|
it("returns 401 for invalid token", async () => {
|
||||||
const handler = createWebhookHandler({
|
const handler = createWebhookHandler({
|
||||||
account: makeAccount(),
|
account: makeAccount(),
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import * as querystring from "node:querystring";
|
import * as querystring from "node:querystring";
|
||||||
|
import {
|
||||||
|
isRequestBodyLimitError,
|
||||||
|
readRequestBodyWithLimit,
|
||||||
|
requestBodyErrorToText,
|
||||||
|
} from "openclaw/plugin-sdk";
|
||||||
import { sendMessage } from "./client.js";
|
import { sendMessage } from "./client.js";
|
||||||
import { validateToken, authorizeUserForDm, sanitizeInput, RateLimiter } from "./security.js";
|
import { validateToken, authorizeUserForDm, sanitizeInput, RateLimiter } from "./security.js";
|
||||||
import type { SynologyWebhookPayload, ResolvedSynologyChatAccount } from "./types.js";
|
import type { SynologyWebhookPayload, ResolvedSynologyChatAccount } from "./types.js";
|
||||||
@@ -34,24 +39,34 @@ export function getSynologyWebhookRateLimiterCountForTest(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Read the full request body as a string. */
|
/** Read the full request body as a string. */
|
||||||
function readBody(req: IncomingMessage): Promise<string> {
|
async function readBody(req: IncomingMessage): Promise<
|
||||||
return new Promise((resolve, reject) => {
|
| { ok: true; body: string }
|
||||||
const chunks: Buffer[] = [];
|
| {
|
||||||
let size = 0;
|
ok: false;
|
||||||
const maxSize = 1_048_576; // 1MB
|
statusCode: number;
|
||||||
|
error: string;
|
||||||
req.on("data", (chunk: Buffer) => {
|
}
|
||||||
size += chunk.length;
|
> {
|
||||||
if (size > maxSize) {
|
try {
|
||||||
req.destroy();
|
const body = await readRequestBodyWithLimit(req, {
|
||||||
reject(new Error("Request body too large"));
|
maxBytes: 1_048_576,
|
||||||
return;
|
timeoutMs: 30_000,
|
||||||
}
|
|
||||||
chunks.push(chunk);
|
|
||||||
});
|
});
|
||||||
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
return { ok: true, body };
|
||||||
req.on("error", reject);
|
} catch (err) {
|
||||||
});
|
if (isRequestBodyLimitError(err)) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
statusCode: err.statusCode,
|
||||||
|
error: requestBodyErrorToText(err.code),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
statusCode: 400,
|
||||||
|
error: "Invalid request body",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse form-urlencoded body into SynologyWebhookPayload. */
|
/** Parse form-urlencoded body into SynologyWebhookPayload. */
|
||||||
@@ -126,17 +141,15 @@ export function createWebhookHandler(deps: WebhookHandlerDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse body
|
// Parse body
|
||||||
let body: string;
|
const body = await readBody(req);
|
||||||
try {
|
if (!body.ok) {
|
||||||
body = await readBody(req);
|
log?.error("Failed to read request body", body.error);
|
||||||
} catch (err) {
|
respond(res, body.statusCode, { error: body.error });
|
||||||
log?.error("Failed to read request body", err);
|
|
||||||
respond(res, 400, { error: "Invalid request body" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse payload
|
// Parse payload
|
||||||
const payload = parsePayload(body);
|
const payload = parsePayload(body.body);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
respond(res, 400, { error: "Missing required fields (token, user_id, text)" });
|
respond(res, 400, { error: "Missing required fields (token, user_id, text)" });
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user