mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 11:37:38 +00:00
msteams: harden webhook ingress timeouts
This commit is contained in:
committed by
Peter Steinberger
parent
ab0b2c21f3
commit
6945ba189d
85
extensions/msteams/src/monitor.test.ts
Normal file
85
extensions/msteams/src/monitor.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { once } from "node:events";
|
||||||
|
import type { Server } from "node:http";
|
||||||
|
import { createConnection, type AddressInfo } from "node:net";
|
||||||
|
import express from "express";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { applyMSTeamsWebhookTimeouts } from "./monitor.js";
|
||||||
|
|
||||||
|
async function closeServer(server: Server): Promise<void> {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server.close(() => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForSlowBodySocketClose(port: number, timeoutMs: number): Promise<number> {
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const socket = createConnection({ host: "127.0.0.1", port }, () => {
|
||||||
|
socket.write("POST /api/messages HTTP/1.1\r\n");
|
||||||
|
socket.write("Host: localhost\r\n");
|
||||||
|
socket.write("Content-Type: application/json\r\n");
|
||||||
|
socket.write("Content-Length: 1048576\r\n");
|
||||||
|
socket.write("\r\n");
|
||||||
|
socket.write('{"type":"message"');
|
||||||
|
});
|
||||||
|
socket.on("error", () => {
|
||||||
|
// ECONNRESET is expected once the server drops the socket.
|
||||||
|
});
|
||||||
|
const failTimer = setTimeout(() => {
|
||||||
|
socket.destroy();
|
||||||
|
reject(new Error(`socket stayed open for ${timeoutMs}ms`));
|
||||||
|
}, timeoutMs);
|
||||||
|
socket.on("close", () => {
|
||||||
|
clearTimeout(failTimer);
|
||||||
|
resolve(Date.now() - startedAt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("msteams monitor webhook hardening", () => {
|
||||||
|
it("applies explicit webhook timeout values", async () => {
|
||||||
|
const app = express();
|
||||||
|
const server = app.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
try {
|
||||||
|
applyMSTeamsWebhookTimeouts(server, {
|
||||||
|
inactivityTimeoutMs: 3210,
|
||||||
|
requestTimeoutMs: 6543,
|
||||||
|
headersTimeoutMs: 9876,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(server.timeout).toBe(3210);
|
||||||
|
expect(server.requestTimeout).toBe(6543);
|
||||||
|
expect(server.headersTimeout).toBe(6543);
|
||||||
|
} finally {
|
||||||
|
await closeServer(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops slow-body webhook requests within configured inactivity timeout", async () => {
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json({ limit: "1mb" }));
|
||||||
|
app.use((_req, res, _next) => {
|
||||||
|
res.status(401).end("unauthorized");
|
||||||
|
});
|
||||||
|
app.post("/api/messages", (_req, res) => {
|
||||||
|
res.end("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = app.listen(0, "127.0.0.1");
|
||||||
|
await once(server, "listening");
|
||||||
|
try {
|
||||||
|
applyMSTeamsWebhookTimeouts(server, {
|
||||||
|
inactivityTimeoutMs: 400,
|
||||||
|
requestTimeoutMs: 1500,
|
||||||
|
headersTimeoutMs: 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
const closedMs = await waitForSlowBodySocketClose(port, 3000);
|
||||||
|
expect(closedMs).toBeLessThan(2500);
|
||||||
|
} finally {
|
||||||
|
await closeServer(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Server } from "node:http";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import {
|
import {
|
||||||
DEFAULT_WEBHOOK_MAX_BODY_BYTES,
|
DEFAULT_WEBHOOK_MAX_BODY_BYTES,
|
||||||
@@ -34,6 +35,31 @@ export type MonitorMSTeamsResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MSTEAMS_WEBHOOK_MAX_BODY_BYTES = DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
const MSTEAMS_WEBHOOK_MAX_BODY_BYTES = DEFAULT_WEBHOOK_MAX_BODY_BYTES;
|
||||||
|
const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 30_000;
|
||||||
|
const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 30_000;
|
||||||
|
const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export type ApplyMSTeamsWebhookTimeoutsOpts = {
|
||||||
|
inactivityTimeoutMs?: number;
|
||||||
|
requestTimeoutMs?: number;
|
||||||
|
headersTimeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function applyMSTeamsWebhookTimeouts(
|
||||||
|
httpServer: Server,
|
||||||
|
opts?: ApplyMSTeamsWebhookTimeoutsOpts,
|
||||||
|
): void {
|
||||||
|
const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
|
||||||
|
const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
|
||||||
|
const headersTimeoutMs = Math.min(
|
||||||
|
opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS,
|
||||||
|
requestTimeoutMs,
|
||||||
|
);
|
||||||
|
|
||||||
|
httpServer.setTimeout(inactivityTimeoutMs);
|
||||||
|
httpServer.requestTimeout = requestTimeoutMs;
|
||||||
|
httpServer.headersTimeout = headersTimeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
export async function monitorMSTeamsProvider(
|
export async function monitorMSTeamsProvider(
|
||||||
opts: MonitorMSTeamsOpts,
|
opts: MonitorMSTeamsOpts,
|
||||||
@@ -289,6 +315,7 @@ export async function monitorMSTeamsProvider(
|
|||||||
httpServer.once("listening", onListening);
|
httpServer.once("listening", onListening);
|
||||||
httpServer.once("error", onError);
|
httpServer.once("error", onError);
|
||||||
});
|
});
|
||||||
|
applyMSTeamsWebhookTimeouts(httpServer);
|
||||||
|
|
||||||
httpServer.on("error", (err) => {
|
httpServer.on("error", (err) => {
|
||||||
log.error("msteams server error", { error: String(err) });
|
log.error("msteams server error", { error: String(err) });
|
||||||
|
|||||||
Reference in New Issue
Block a user