Files
openclaw/extensions/discord/src/proxy-fetch.ts
Vincent Koc 50e1eb56d7 fix(security): harden discord proxy and bundled channel activation (#60455)
* fix(security): tighten discord proxy and mobile tls guards

* fix(plugins): enforce allowlists for bundled channels

* fix(types): align callers with removed legacy config aliases

* fix(security): preserve bundled channel opt-in and ipv6 proxies
2026-04-04 02:48:52 +09:00

92 lines
2.8 KiB
TypeScript

import { isIP } from "node:net";
import { type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { makeProxyFetch } from "openclaw/plugin-sdk/infra-runtime";
import { danger } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import type { ResolvedDiscordAccount } from "./accounts.js";
export function resolveDiscordProxyUrl(
account: Pick<ResolvedDiscordAccount, "config">,
cfg?: OpenClawConfig,
): string | undefined {
const accountProxy = account.config.proxy?.trim();
if (accountProxy) {
return accountProxy;
}
const channelProxy = cfg?.channels?.discord?.proxy;
if (typeof channelProxy !== "string") {
return undefined;
}
const trimmed = channelProxy.trim();
return trimmed || undefined;
}
export function resolveDiscordProxyFetchByUrl(
proxyUrl: string | undefined,
runtime?: Pick<RuntimeEnv, "error">,
): typeof fetch | undefined {
return withValidatedDiscordProxy(proxyUrl, runtime, (proxy) => makeProxyFetch(proxy));
}
export function resolveDiscordProxyFetchForAccount(
account: Pick<ResolvedDiscordAccount, "config">,
cfg?: OpenClawConfig,
runtime?: Pick<RuntimeEnv, "error">,
): typeof fetch | undefined {
return resolveDiscordProxyFetchByUrl(resolveDiscordProxyUrl(account, cfg), runtime);
}
export function withValidatedDiscordProxy<T>(
proxyUrl: string | undefined,
runtime: Pick<RuntimeEnv, "error"> | undefined,
createValue: (proxyUrl: string) => T,
): T | undefined {
const proxy = proxyUrl?.trim();
if (!proxy) {
return undefined;
}
try {
validateDiscordProxyUrl(proxy);
return createValue(proxy);
} catch (err) {
runtime?.error?.(danger(`discord: invalid rest proxy: ${String(err)}`));
return undefined;
}
}
export function validateDiscordProxyUrl(proxyUrl: string): string {
let parsed: URL;
try {
parsed = new URL(proxyUrl);
} catch {
throw new Error("Proxy URL must be a valid http or https URL");
}
if (!["http:", "https:"].includes(parsed.protocol)) {
throw new Error("Proxy URL must use http or https");
}
if (!isLoopbackProxyHostname(parsed.hostname)) {
throw new Error("Proxy URL must target a loopback host");
}
return proxyUrl;
}
function isLoopbackProxyHostname(hostname: string): boolean {
const normalized = hostname.trim().toLowerCase();
if (!normalized) {
return false;
}
const bracketless =
normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
if (bracketless === "localhost") {
return true;
}
const ipFamily = isIP(bracketless);
if (ipFamily === 4) {
return bracketless.startsWith("127.");
}
if (ipFamily === 6) {
return bracketless === "::1" || bracketless === "0:0:0:0:0:0:0:1";
}
return false;
}