mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix: route Discord gateway metadata through proxy (#86601)
* fix: route Discord gateway metadata through proxy * fix: keep Discord gateway proxy fetch guarded
This commit is contained in:
committed by
GitHub
parent
f00a912c25
commit
5b6d409248
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord: route gateway metadata REST lookups through the configured Discord proxy so proxied accounts do not fall back to direct `discord.com` connections before opening the WebSocket. Fixes #80227. Thanks @Clivilwalker.
|
||||
- Agents/media: hydrate current-turn image attachments from filename-derived MIME types so active vision can see generated or forwarded images whose source omitted an image content type. (#84812) Thanks @marchpure.
|
||||
- Agents/fs: point workspace-only scratch-path guidance at in-workspace temp directories while keeping host-root writes rejected by the tool guard. (#86501) Thanks @tianxiaochannel-oss88.
|
||||
- Agents/media: keep async cron media completions scoped to their run session while preserving direct delivery for stale generated-media success and failure notifications. (#86529) Thanks @ai-hpc.
|
||||
|
||||
@@ -24,6 +24,10 @@ export type DiscordGatewayFetch = (
|
||||
input: string,
|
||||
init?: DiscordGatewayFetchInit,
|
||||
) => Promise<DiscordGatewayMetadataResponse>;
|
||||
export type DiscordGatewayMetadataFetchOptions = {
|
||||
capture?: false | { flowId: string; meta: Record<string, unknown> };
|
||||
proxyUrl?: string;
|
||||
};
|
||||
|
||||
type DiscordGatewayMetadataError = Error & { transient?: boolean };
|
||||
|
||||
@@ -265,10 +269,10 @@ export function resolveGatewayInfoWithFallback(params: { runtime?: RuntimeEnv; e
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchDiscordGatewayMetadataDirect(
|
||||
export async function fetchDiscordGatewayMetadataGuarded(
|
||||
input: string,
|
||||
init?: DiscordGatewayFetchInit,
|
||||
capture?: false | { flowId: string; meta: Record<string, unknown> },
|
||||
options?: DiscordGatewayMetadataFetchOptions,
|
||||
): Promise<Response> {
|
||||
const guarded = await fetchWithSsrFGuard({
|
||||
url: resolveFetchInputUrl(input),
|
||||
@@ -276,6 +280,16 @@ export async function fetchDiscordGatewayMetadataDirect(
|
||||
policy: { allowedHostnames: [DISCORD_API_HOST] },
|
||||
capture: false,
|
||||
auditContext: "discord.gateway.metadata",
|
||||
...(options?.proxyUrl
|
||||
? {
|
||||
mode: "trusted_explicit_proxy" as const,
|
||||
dispatcherPolicy: {
|
||||
mode: "explicit-proxy" as const,
|
||||
proxyUrl: options.proxyUrl,
|
||||
allowPrivateProxy: true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -283,15 +297,15 @@ export async function fetchDiscordGatewayMetadataDirect(
|
||||
} finally {
|
||||
await guarded.release();
|
||||
}
|
||||
if (capture) {
|
||||
if (options?.capture) {
|
||||
captureHttpExchange({
|
||||
url: input,
|
||||
method: (init?.method as string | undefined) ?? "GET",
|
||||
requestHeaders: init?.headers as Headers | Record<string, string> | undefined,
|
||||
requestBody: (init as RequestInit & { body?: BodyInit | null })?.body ?? null,
|
||||
response,
|
||||
flowId: capture.flowId,
|
||||
meta: capture.meta,
|
||||
flowId: options.capture.flowId,
|
||||
meta: options.capture.meta,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { resolveDiscordVoiceEnabled } from "../voice/config.js";
|
||||
import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js";
|
||||
import {
|
||||
fetchDiscordGatewayInfoWithTimeout,
|
||||
fetchDiscordGatewayMetadataDirect,
|
||||
fetchDiscordGatewayMetadataGuarded,
|
||||
resolveDiscordGatewayInfoTimeoutMs,
|
||||
resolveGatewayInfoWithFallback,
|
||||
type DiscordGatewayFetch,
|
||||
@@ -230,18 +230,22 @@ function createGatewayPlugin(params: {
|
||||
return new OpenClawGatewayPlugin();
|
||||
}
|
||||
|
||||
function createDiscordGatewayMetadataFetch(debugCaptureEnabled: boolean): DiscordGatewayFetch {
|
||||
function createDiscordGatewayMetadataFetch(
|
||||
debugCaptureEnabled: boolean,
|
||||
proxyUrl?: string,
|
||||
): DiscordGatewayFetch {
|
||||
return (input, init) =>
|
||||
fetchDiscordGatewayMetadataDirect(
|
||||
input,
|
||||
init,
|
||||
debugCaptureEnabled
|
||||
? false
|
||||
fetchDiscordGatewayMetadataGuarded(input, init, {
|
||||
...(debugCaptureEnabled
|
||||
? {}
|
||||
: {
|
||||
flowId: randomUUID(),
|
||||
meta: { subsystem: "discord-gateway-metadata" },
|
||||
},
|
||||
);
|
||||
capture: {
|
||||
flowId: randomUUID(),
|
||||
meta: { subsystem: "discord-gateway-metadata" },
|
||||
},
|
||||
}),
|
||||
...(proxyUrl ? { proxyUrl } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function waitForDiscordGatewayPluginRegistration(
|
||||
@@ -279,10 +283,12 @@ export function createDiscordGatewayPlugin(params: {
|
||||
const HttpsProxyAgentCtor =
|
||||
params.testing?.HttpsProxyAgentCtor ?? httpsProxyAgent.HttpsProxyAgent;
|
||||
wsAgent = new HttpsProxyAgentCtor<string>(proxy);
|
||||
fetchImpl = createDiscordGatewayMetadataFetch(debugProxySettings.enabled, proxy);
|
||||
params.runtime.log?.("discord: gateway proxy enabled");
|
||||
} catch (err) {
|
||||
params.runtime.error?.(danger(`discord: invalid gateway proxy: ${String(err)}`));
|
||||
fetchImpl = (input, init) => fetchDiscordGatewayMetadataDirect(input, init, false);
|
||||
fetchImpl = (input, init) =>
|
||||
fetchDiscordGatewayMetadataGuarded(input, init, { capture: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const {
|
||||
globalFetchMock,
|
||||
HttpsAgent,
|
||||
HttpsProxyAgent,
|
||||
fetchWithSsrFGuardMock,
|
||||
getLastAgent,
|
||||
getLastProxyAgent,
|
||||
resolveDebugProxySettingsMock,
|
||||
@@ -53,6 +54,18 @@ const {
|
||||
const captureHttpExchangeSpy = vi.fn();
|
||||
const captureWsEventSpy = vi.fn();
|
||||
const resolveDebugProxySettingsMock = vi.fn(() => ({ enabled: false }));
|
||||
const fetchWithSsrFGuardMock = vi.fn(async (params: { url: string; init?: RequestInit }) => {
|
||||
const source = (await globalFetchMock(params.url, params.init)) as Response;
|
||||
const body = await source.text();
|
||||
return {
|
||||
response: new Response(body, {
|
||||
status: source.status,
|
||||
statusText: source.statusText,
|
||||
headers: source.headers,
|
||||
}),
|
||||
release: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const GatewayIntents = {
|
||||
Guilds: 1 << 0,
|
||||
@@ -114,6 +127,7 @@ const {
|
||||
globalFetchMock,
|
||||
HttpsAgent,
|
||||
HttpsProxyAgent,
|
||||
fetchWithSsrFGuardMock,
|
||||
getLastAgent: () => HttpsAgent.lastCreated,
|
||||
getLastProxyAgent: () => HttpsProxyAgent.lastCreated,
|
||||
captureHttpExchangeSpy,
|
||||
@@ -166,18 +180,7 @@ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: vi.fn(async (params: { url: string; init?: RequestInit }) => {
|
||||
const source = (await globalFetchMock(params.url, params.init)) as Response;
|
||||
const body = await source.text();
|
||||
return {
|
||||
response: new Response(body, {
|
||||
status: source.status,
|
||||
statusText: source.statusText,
|
||||
headers: source.headers,
|
||||
}),
|
||||
release: vi.fn(),
|
||||
};
|
||||
}),
|
||||
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
||||
}));
|
||||
|
||||
describe("createDiscordGatewayPlugin", () => {
|
||||
@@ -213,6 +216,16 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
return firstMockCall(mock, label)[index];
|
||||
}
|
||||
|
||||
function firstGuardedFetchCall() {
|
||||
return firstMockArg(fetchWithSsrFGuardMock, "fetchWithSsrFGuardMock") as {
|
||||
url: string;
|
||||
init?: RequestInit & { signal?: unknown };
|
||||
mode?: string;
|
||||
dispatcherPolicy?: unknown;
|
||||
policy?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
function createProxyTestingOverrides() {
|
||||
return {
|
||||
HttpsProxyAgentCtor:
|
||||
@@ -318,6 +331,7 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
vi.useRealTimers();
|
||||
baseRegisterClientSpy.mockClear();
|
||||
globalFetchMock.mockClear();
|
||||
fetchWithSsrFGuardMock.mockClear();
|
||||
httpsAgentSpy.mockClear();
|
||||
wsProxyAgentSpy.mockClear();
|
||||
webSocketSpy.mockClear();
|
||||
@@ -504,9 +518,10 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype);
|
||||
expect(runtime.error).toHaveBeenCalled();
|
||||
expect(runtime.log).not.toHaveBeenCalled();
|
||||
expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps gateway metadata lookup on the guarded direct fetch when proxy is configured", async () => {
|
||||
it("routes gateway metadata lookup through the guarded proxy dispatcher", async () => {
|
||||
const runtime = createRuntime();
|
||||
const plugin = createDiscordGatewayPlugin({
|
||||
discordConfig: { proxy: "http://127.0.0.1:8080" },
|
||||
@@ -516,15 +531,18 @@ describe("createDiscordGatewayPlugin", () => {
|
||||
|
||||
await registerGatewayClientWithMetadata({ plugin, fetchMock: globalFetchMock });
|
||||
|
||||
expect(globalFetchMock).toHaveBeenCalledTimes(1);
|
||||
const fetchInit = firstMockArg(globalFetchMock, "globalFetchMock", 1) as
|
||||
| { headers?: Record<string, string>; signal?: unknown }
|
||||
| undefined;
|
||||
expect(firstMockArg(globalFetchMock, "globalFetchMock")).toBe(
|
||||
"https://discord.com/api/v10/gateway/bot",
|
||||
);
|
||||
expect(fetchInit?.headers).toEqual({ Authorization: "Bot token-123" });
|
||||
expect(fetchInit?.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(1);
|
||||
const guardedFetch = firstGuardedFetchCall();
|
||||
expect(guardedFetch.url).toBe("https://discord.com/api/v10/gateway/bot");
|
||||
expect(guardedFetch.mode).toBe("trusted_explicit_proxy");
|
||||
expect(guardedFetch.dispatcherPolicy).toEqual({
|
||||
mode: "explicit-proxy",
|
||||
proxyUrl: "http://127.0.0.1:8080",
|
||||
allowPrivateProxy: true,
|
||||
});
|
||||
expect(guardedFetch.policy).toEqual({ allowedHostnames: ["discord.com"] });
|
||||
expect(guardedFetch.init?.headers).toEqual({ Authorization: "Bot token-123" });
|
||||
expect(guardedFetch.init?.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user