openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
394 lines (393 loc) • 17.5 kB
JavaScript
import { a as logWarn } from "./logger-lqqYRtFw.js";
import { f as isLoopbackIpAddress } from "./ip-0oQXo6_w.js";
import { c as shouldUseEnvHttpProxyForUrl, i as hasProxyEnvConfigured } from "./proxy-env-B9aW4MXJ.js";
import { t as getActiveManagedProxyLoopbackMode } from "./active-proxy-state-DJLhrP_Z.js";
import { n as createHttp1EnvHttpProxyAgent, r as createHttp1ProxyAgent, t as createHttp1Agent } from "./undici-runtime-CdQVyjAR.js";
import { o as globalUndiciStreamTimeoutMs } from "./undici-global-dispatcher-DTahNL39.js";
import { n as buildTimeoutAbortSignal } from "./fetch-timeout-CDaw0X8V.js";
import { n as normalizeRequestInitHeadersForFetch, t as normalizeHeadersInitForFetch } from "./fetch-headers-DPnOMwOE.js";
import { _ as resolvePinnedHostnameWithPolicy, a as createPinnedDispatcher, i as closeDispatcher, n as assertHostnameAllowedWithPolicy, p as matchesHostnameAllowlist, t as SsrFBlockedError, v as resolveSsrFPolicyForUrl } from "./ssrf-CvPEXMGn.js";
import { t as retainSafeHeadersForCrossOriginRedirect$1 } from "./redirect-headers-BjUyzEnF.js";
import { r as isMockedFetch, t as fetchWithRuntimeDispatcher } from "./runtime-fetch-BwqSVSNK.js";
//#region src/infra/net/configured-local-origin-bypass.ts
function resolveHttpOrigin(value) {
try {
const parsed = new URL(value.trim());
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
return parsed.origin.toLowerCase();
} catch {
return;
}
}
function isLoopbackManagedProxyBypassHost(hostname) {
const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "").replace(/^\[(.*)\]$/, "$1");
return normalized === "localhost" || isLoopbackIpAddress(normalized);
}
function isExactConfiguredLocalOriginBypass(params) {
if (params.managedProxyBypass?.kind !== "configured-local-origin") return false;
const baseOrigin = resolveHttpOrigin(params.managedProxyBypass.baseUrl);
if (!baseOrigin) return false;
let baseHostname;
try {
baseHostname = new URL(params.managedProxyBypass.baseUrl.trim()).hostname;
} catch {
return false;
}
if (!isLoopbackManagedProxyBypassHost(baseHostname)) return false;
return resolveHttpOrigin(params.url.toString()) === baseOrigin;
}
function isPinnedLoopbackTarget(addresses) {
return addresses.length > 0 && addresses.every((address) => isLoopbackIpAddress(address));
}
/** Return whether a configured local provider origin may bypass the managed proxy. */
function shouldUseConfiguredLocalOriginManagedProxyBypass(params) {
if (!isExactConfiguredLocalOriginBypass(params)) return false;
const loopbackMode = getActiveManagedProxyLoopbackMode();
if (loopbackMode === "proxy") return false;
if (loopbackMode === "block" && isLoopbackManagedProxyBypassHost(params.url.hostname)) throw new SsrFBlockedError("proxy: configured local provider loopback connections are blocked by proxy.loopbackMode");
return isPinnedLoopbackTarget(params.resolvedAddresses);
}
//#endregion
//#region src/infra/net/fetch-guard.ts
function resolveDispatcherTimeoutMs(fromParams) {
if (fromParams !== void 0) return fromParams;
if (globalUndiciStreamTimeoutMs !== void 0) return globalUndiciStreamTimeoutMs;
}
const GUARDED_FETCH_MODE = {
STRICT: "strict",
TRUSTED_ENV_PROXY: "trusted_env_proxy",
TRUSTED_EXPLICIT_PROXY: "trusted_explicit_proxy"
};
const DEFAULT_MAX_REDIRECTS = 3;
const OPENCLAW_DEBUG_PROXY_ENABLED = "OPENCLAW_DEBUG_PROXY_ENABLED";
function getRedirectVisitKey(url, init) {
return `${init?.method?.toUpperCase() ?? "GET"} ${url}`;
}
function isTruthyEnvValue(value) {
return value === "1" || value === "true" || value === "yes" || value === "on";
}
function withStrictGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.STRICT
};
}
function withTrustedEnvProxyGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY
};
}
function withTrustedExplicitProxyGuardedFetchMode(params) {
return {
...params,
mode: GUARDED_FETCH_MODE.TRUSTED_EXPLICIT_PROXY
};
}
function resolveGuardedFetchMode(params) {
if (params.mode) return params.mode;
if (params.proxy === "env" && params.dangerouslyAllowEnvProxyWithoutPinnedDns === true) return GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY;
return GUARDED_FETCH_MODE.STRICT;
}
function isManagedProxyActive() {
return process.env["OPENCLAW_PROXY_ACTIVE"] === "1";
}
function assertExplicitProxySupportsPinnedDns(url, dispatcherPolicy, pinDns) {
if (pinDns !== false && dispatcherPolicy?.mode === "explicit-proxy" && url.protocol !== "https:") throw new Error("Explicit proxy SSRF pinning requires HTTPS targets; plain HTTP targets are not supported");
}
function createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs) {
if (!dispatcherPolicy) return null;
if (dispatcherPolicy.mode === "direct") return createHttp1Agent(dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : void 0, timeoutMs);
if (dispatcherPolicy.mode === "env-proxy") return createHttp1EnvHttpProxyAgent({
...dispatcherPolicy.connect ? { connect: { ...dispatcherPolicy.connect } } : {},
...dispatcherPolicy.proxyTls ? { proxyTls: { ...dispatcherPolicy.proxyTls } } : {}
}, timeoutMs);
const proxyUrl = dispatcherPolicy.proxyUrl.trim();
if (dispatcherPolicy.proxyTls) return createHttp1ProxyAgent({
uri: proxyUrl,
requestTls: { ...dispatcherPolicy.proxyTls }
}, timeoutMs);
return createHttp1ProxyAgent({ uri: proxyUrl }, timeoutMs);
}
async function assertExplicitProxyAllowed(dispatcherPolicy, lookupFn, policy) {
if (!dispatcherPolicy || dispatcherPolicy.mode !== "explicit-proxy") return;
let parsedProxyUrl;
try {
parsedProxyUrl = new URL(dispatcherPolicy.proxyUrl);
} catch {
throw new Error("Invalid explicit proxy URL");
}
if (!["http:", "https:"].includes(parsedProxyUrl.protocol)) throw new Error("Explicit proxy URL must use http or https");
const proxyPolicy = policy || dispatcherPolicy.allowPrivateProxy === true ? {
...policy,
hostnameAllowlist: void 0,
...dispatcherPolicy.allowPrivateProxy === true ? { allowPrivateNetwork: true } : {}
} : void 0;
await resolvePinnedHostnameWithPolicy(parsedProxyUrl.hostname, {
lookupFn,
policy: proxyPolicy
});
}
function isRedirectStatus(status) {
return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
}
function isAmbientGlobalFetch(params) {
return typeof params.fetchImpl === "function" && typeof params.globalFetch === "function" && params.fetchImpl === params.globalFetch;
}
function retainSafeHeadersForCrossOriginRedirectHeaders(headers) {
return retainSafeHeadersForCrossOriginRedirect$1(headers);
}
async function captureGuardedFetchExchange(params) {
if (params.capture === false || !isTruthyEnvValue(process.env[OPENCLAW_DEBUG_PROXY_ENABLED])) return;
const { captureHttpExchange, isDebugProxyGlobalFetchPatchInstalled } = await import("./runtime-CknvMuBf.js");
if (params.capturedByGlobalFetchPatch && isDebugProxyGlobalFetchPatchInstalled()) return;
captureHttpExchange({
url: params.url,
method: params.method,
requestHeaders: params.requestHeaders,
requestBody: params.requestBody,
response: params.response,
transport: params.transport,
flowId: params.capture?.flowId,
meta: {
captureOrigin: "guarded-fetch",
...params.auditContext ? { auditContext: params.auditContext } : {},
...params.capture?.meta
}
});
}
function retainSafeHeadersForCrossOriginRedirect(init) {
if (!init?.headers) return init;
return {
...init,
headers: retainSafeHeadersForCrossOriginRedirect$1(init.headers)
};
}
function resolveRetainedAuthorizationForRedirect(params) {
const init = params.init;
if (!init?.headers || !params.hostnameAllowlist?.length) return;
if (params.nextUrl.protocol !== "https:") return;
if (!params.hostnameAllowlist.includes("*") && !matchesHostnameAllowlist(params.nextUrl.hostname, params.hostnameAllowlist)) return;
const normalizedInit = normalizeRequestInitHeadersForFetch(init);
if (!normalizedInit?.headers) return;
return new Headers(normalizedInit.headers).get("authorization") ?? void 0;
}
function restoreRedirectAuthorization(params) {
if (!params.authorization) return params.init;
const headers = new Headers(params.init?.headers);
headers.set("Authorization", params.authorization);
return {
...params.init,
headers
};
}
function dropBodyHeaders(headers) {
if (!headers) return headers;
const nextHeaders = new Headers(normalizeHeadersInitForFetch(headers));
nextHeaders.delete("content-encoding");
nextHeaders.delete("content-language");
nextHeaders.delete("content-length");
nextHeaders.delete("content-location");
nextHeaders.delete("content-type");
nextHeaders.delete("transfer-encoding");
return nextHeaders;
}
function rewriteRedirectInitForMethod(params) {
const { init, status } = params;
if (!init) return init;
const currentMethod = init.method?.toUpperCase() ?? "GET";
if (!(status === 303 ? currentMethod !== "GET" && currentMethod !== "HEAD" : (status === 301 || status === 302) && currentMethod === "POST")) return init;
return {
...init,
method: "GET",
body: void 0,
headers: dropBodyHeaders(init.headers)
};
}
function rewriteRedirectInitForCrossOrigin(params) {
const { init, allowUnsafeReplay } = params;
if (!init || allowUnsafeReplay) return init;
const currentMethod = init.method?.toUpperCase() ?? "GET";
if (currentMethod === "GET" || currentMethod === "HEAD") return init;
return {
...init,
body: void 0,
headers: dropBodyHeaders(init.headers)
};
}
async function fetchWithSsrFGuard(params) {
const { managedProxyBypass: _ignoredManagedProxyBypass, ...publicParams } = params;
return await fetchWithSsrFGuardInternal(publicParams);
}
async function fetchConfiguredLocalOriginWithSsrFGuard({ configuredLocalOriginBaseUrl, ...params }) {
return await fetchWithSsrFGuardInternal({
...params,
managedProxyBypass: {
kind: "configured-local-origin",
baseUrl: configuredLocalOriginBaseUrl
}
});
}
async function fetchWithSsrFGuardInternal(params) {
const defaultFetch = params.fetchImpl ?? globalThis.fetch;
if (!defaultFetch) throw new Error("fetch is not available");
const isUsingMockedFetch = isMockedFetch(defaultFetch);
const maxRedirects = typeof params.maxRedirects === "number" && Number.isFinite(params.maxRedirects) ? Math.max(0, Math.floor(params.maxRedirects)) : DEFAULT_MAX_REDIRECTS;
const mode = resolveGuardedFetchMode(params);
const { signal, cleanup, refresh } = buildTimeoutAbortSignal({
timeoutMs: params.timeoutMs,
signal: params.signal,
operation: "fetchWithSsrFGuard",
url: params.url
});
let released = false;
const release = async (dispatcher) => {
if (released) return;
released = true;
cleanup();
await closeDispatcher(dispatcher ?? void 0);
};
let currentUrl = params.url;
let currentInit = normalizeRequestInitHeadersForFetch(params.init ? { ...params.init } : void 0);
const visited = new Set([getRedirectVisitKey(currentUrl, currentInit)]);
let redirectCount = 0;
while (true) {
let parsedUrl;
try {
parsedUrl = new URL(currentUrl);
} catch {
await release();
throw new Error("Invalid URL: must be http or https");
}
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
await release();
throw new Error("Invalid URL: must be http or https");
}
if (params.requireHttps === true && parsedUrl.protocol !== "https:") {
await release();
throw new Error("URL must use https");
}
let dispatcher = null;
const policyForUrl = resolveSsrFPolicyForUrl(parsedUrl, params.policy);
const dispatcherPolicy = params.resolveDispatcherPolicy?.(parsedUrl) ?? params.dispatcherPolicy;
try {
const usesTrustedExplicitProxyMode = mode === GUARDED_FETCH_MODE.TRUSTED_EXPLICIT_PROXY && dispatcherPolicy?.mode === "explicit-proxy";
assertExplicitProxySupportsPinnedDns(parsedUrl, dispatcherPolicy, usesTrustedExplicitProxyMode ? false : params.pinDns);
await assertExplicitProxyAllowed(dispatcherPolicy, params.lookupFn, params.policy);
const canUseManagedProxy = mode === GUARDED_FETCH_MODE.STRICT && isManagedProxyActive() && hasProxyEnvConfigured();
const canUseTrustedEnvProxy = (mode === GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY || params.useEnvProxyForEligibleUrls === true && !canUseManagedProxy) && !dispatcherPolicy && shouldUseEnvHttpProxyForUrl(parsedUrl.toString());
const canUseMockedFetchWithoutDns = isUsingMockedFetch && params.lookupFn === void 0 && !canUseTrustedEnvProxy && !canUseManagedProxy && !usesTrustedExplicitProxyMode && params.pinDns !== false;
const timeoutMs = resolveDispatcherTimeoutMs(params.timeoutMs);
if (canUseTrustedEnvProxy || params.pinDns === false) assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
if (canUseTrustedEnvProxy) dispatcher = createHttp1EnvHttpProxyAgent(void 0, timeoutMs);
else if (canUseManagedProxy) {
const pinned = await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
lookupFn: params.lookupFn,
policy: policyForUrl
});
dispatcher = shouldUseConfiguredLocalOriginManagedProxyBypass({
url: parsedUrl,
managedProxyBypass: params.managedProxyBypass,
resolvedAddresses: pinned.addresses
}) ? createPinnedDispatcher(pinned, dispatcherPolicy, policyForUrl, timeoutMs) : createHttp1EnvHttpProxyAgent(void 0, timeoutMs);
} else if (usesTrustedExplicitProxyMode) {
assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
dispatcher = createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs);
} else if (canUseMockedFetchWithoutDns) assertHostnameAllowedWithPolicy(parsedUrl.hostname, policyForUrl);
else if (params.pinDns === false) {
await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
lookupFn: params.lookupFn,
policy: policyForUrl
});
dispatcher = createPolicyDispatcherWithoutPinnedDns(dispatcherPolicy, timeoutMs);
} else dispatcher = createPinnedDispatcher(await resolvePinnedHostnameWithPolicy(parsedUrl.hostname, {
lookupFn: params.lookupFn,
policy: policyForUrl
}), dispatcherPolicy, policyForUrl, timeoutMs);
const init = {
...currentInit ? { ...currentInit } : {},
redirect: "manual",
...dispatcher ? { dispatcher } : {},
...signal ? { signal } : {}
};
const supportsDispatcherInit = params.fetchImpl !== void 0 && !isAmbientGlobalFetch({
fetchImpl: params.fetchImpl,
globalFetch: globalThis.fetch
}) || isUsingMockedFetch;
const shouldUseRuntimeFetch = Boolean(dispatcher) && !supportsDispatcherInit;
const response = shouldUseRuntimeFetch ? await fetchWithRuntimeDispatcher(parsedUrl.toString(), init) : await defaultFetch(parsedUrl.toString(), init);
const capturedByGlobalFetchPatch = !shouldUseRuntimeFetch && isAmbientGlobalFetch({
fetchImpl: defaultFetch,
globalFetch: globalThis.fetch
});
await captureGuardedFetchExchange({
url: parsedUrl.toString(),
method: currentInit?.method ?? "GET",
requestHeaders: currentInit?.headers,
requestBody: currentInit?.body ?? null,
response,
transport: "http",
capture: params.capture,
auditContext: params.auditContext,
capturedByGlobalFetchPatch
});
if (isRedirectStatus(response.status)) {
const location = response.headers.get("location");
if (!location) {
await release(dispatcher);
throw new Error(`Redirect missing location header (${response.status})`);
}
redirectCount += 1;
if (redirectCount > maxRedirects) {
await release(dispatcher);
throw new Error(`Too many redirects (limit: ${maxRedirects})`);
}
const nextParsedUrl = new URL(location, parsedUrl);
const nextUrl = nextParsedUrl.toString();
const retainedAuthorization = resolveRetainedAuthorizationForRedirect({
init: currentInit,
nextUrl: nextParsedUrl,
hostnameAllowlist: params.retainAuthorizationRedirectHostnameAllowlist
});
currentInit = rewriteRedirectInitForMethod({
init: currentInit,
status: response.status
});
if (nextParsedUrl.origin !== parsedUrl.origin) {
currentInit = rewriteRedirectInitForCrossOrigin({
init: currentInit,
allowUnsafeReplay: params.allowCrossOriginUnsafeRedirectReplay === true
});
currentInit = retainSafeHeadersForCrossOriginRedirect(currentInit);
currentInit = restoreRedirectAuthorization({
init: currentInit,
authorization: retainedAuthorization
});
}
const nextVisitKey = getRedirectVisitKey(nextUrl, currentInit);
if (visited.has(nextVisitKey)) {
await release(dispatcher);
throw new Error("Redirect loop detected");
}
visited.add(nextVisitKey);
response.body?.cancel();
await closeDispatcher(dispatcher);
currentUrl = nextUrl;
continue;
}
return {
response,
finalUrl: currentUrl,
release: async () => release(dispatcher),
refreshTimeout: refresh
};
} catch (err) {
if (err instanceof SsrFBlockedError) logWarn(`security: blocked URL fetch (${params.auditContext ?? "url-fetch"}) targetOrigin=${parsedUrl.origin} reason=${err.message}`);
await release(dispatcher);
throw err;
}
}
}
//#endregion
export { withStrictGuardedFetchMode as a, retainSafeHeadersForCrossOriginRedirectHeaders as i, fetchConfiguredLocalOriginWithSsrFGuard as n, withTrustedEnvProxyGuardedFetchMode as o, fetchWithSsrFGuard as r, withTrustedExplicitProxyGuardedFetchMode as s, GUARDED_FETCH_MODE as t };