mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix: harden msteams webhook ingress lifecycle integration (#25960) (thanks @bmendonca3)
This commit is contained in:
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- MSTeams/webhook ingress hardening: apply explicit HTTP server timeouts, run auth middleware before JSON body parsing, and keep timeout/lifecycle wiring compatible with gateway-managed monitor startup. (#25960) Thanks @bmendonca3.
|
||||
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
|
||||
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
|
||||
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.
|
||||
|
||||
@@ -6,6 +6,10 @@ import type { MSTeamsPollStore } from "./polls.js";
|
||||
|
||||
type FakeServer = EventEmitter & {
|
||||
close: (callback?: (err?: Error | null) => void) => void;
|
||||
setTimeout: (msecs: number, callback?: () => void) => FakeServer;
|
||||
timeout: number;
|
||||
requestTimeout: number;
|
||||
headersTimeout: number;
|
||||
};
|
||||
|
||||
const expressControl = vi.hoisted(() => ({
|
||||
@@ -31,6 +35,13 @@ vi.mock("express", () => {
|
||||
post: vi.fn(),
|
||||
listen: vi.fn((_port: number) => {
|
||||
const server = new EventEmitter() as FakeServer;
|
||||
server.timeout = 0;
|
||||
server.requestTimeout = 0;
|
||||
server.headersTimeout = 0;
|
||||
server.setTimeout = vi.fn((msecs: number) => {
|
||||
server.timeout = msecs;
|
||||
return server;
|
||||
});
|
||||
server.close = (callback?: (err?: Error | null) => void) => {
|
||||
queueMicrotask(() => {
|
||||
server.emit("close");
|
||||
|
||||
@@ -37,7 +37,7 @@ async function waitForSlowBodySocketClose(port: number, timeoutMs: number): Prom
|
||||
}
|
||||
|
||||
describe("msteams monitor webhook hardening", () => {
|
||||
it("applies explicit webhook timeout values", async () => {
|
||||
it("applies timeout values and clamps headersTimeout to requestTimeout", async () => {
|
||||
const app = express();
|
||||
const server = app.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
|
||||
@@ -268,6 +268,7 @@ export async function monitorMSTeamsProvider(
|
||||
|
||||
// Create Express server
|
||||
const expressApp = express.default();
|
||||
expressApp.use(authorizeJWT(authConfig));
|
||||
expressApp.use(express.json({ limit: MSTEAMS_WEBHOOK_MAX_BODY_BYTES }));
|
||||
expressApp.use((err: unknown, _req: Request, res: Response, next: (err?: unknown) => void) => {
|
||||
if (err && typeof err === "object" && "status" in err && err.status === 413) {
|
||||
@@ -276,7 +277,6 @@ export async function monitorMSTeamsProvider(
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
expressApp.use(authorizeJWT(authConfig));
|
||||
|
||||
// Set up the messages endpoint - use configured path and /api/messages as fallback
|
||||
const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
|
||||
@@ -301,9 +301,9 @@ export async function monitorMSTeamsProvider(
|
||||
|
||||
// Start listening and fail fast if bind/listen fails.
|
||||
const httpServer = expressApp.listen(port);
|
||||
applyMSTeamsWebhookTimeouts(httpServer);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onListening = () => {
|
||||
httpServer.off("error", onError);
|
||||
log.info(`msteams provider started on port ${port}`);
|
||||
resolve();
|
||||
};
|
||||
@@ -315,8 +315,6 @@ export async function monitorMSTeamsProvider(
|
||||
httpServer.once("listening", onListening);
|
||||
httpServer.once("error", onError);
|
||||
});
|
||||
applyMSTeamsWebhookTimeouts(httpServer);
|
||||
|
||||
httpServer.on("error", (err) => {
|
||||
log.error("msteams server error", { error: String(err) });
|
||||
});
|
||||
@@ -333,24 +331,30 @@ export async function monitorMSTeamsProvider(
|
||||
});
|
||||
};
|
||||
|
||||
// Handle abort signal
|
||||
const onAbort = () => {
|
||||
void shutdown();
|
||||
};
|
||||
if (opts.abortSignal) {
|
||||
if (opts.abortSignal.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
// Some direct callers may invoke monitor without lifecycle wiring.
|
||||
// Return immediately so they can call shutdown explicitly.
|
||||
if (!opts.abortSignal) {
|
||||
return { app: expressApp, shutdown };
|
||||
}
|
||||
|
||||
// Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit.
|
||||
await new Promise<void>((resolve) => {
|
||||
const closePromise = new Promise<void>((resolve) => {
|
||||
httpServer.once("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle abort signal
|
||||
const onAbort = () => {
|
||||
void shutdown();
|
||||
};
|
||||
if (opts.abortSignal.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
|
||||
// Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit.
|
||||
await closePromise;
|
||||
opts.abortSignal?.removeEventListener("abort", onAbort);
|
||||
|
||||
return { app: expressApp, shutdown };
|
||||
|
||||
Reference in New Issue
Block a user