mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 15:41:40 +00:00
test: inject web fetch dns lookup seams
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { LookupFn } from "../../infra/net/ssrf.js";
|
||||
import * as logger from "../../logger.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import {
|
||||
createBaseWebFetchToolConfig,
|
||||
installWebFetchSsrfHarness,
|
||||
makeFetchHeaders,
|
||||
} from "./web-fetch.test-harness.js";
|
||||
import { createBaseWebFetchToolConfig, makeFetchHeaders } from "./web-fetch.test-harness.js";
|
||||
import "./web-fetch.test-mocks.js";
|
||||
import { createWebFetchTool } from "./web-tools.js";
|
||||
|
||||
const baseToolConfig = createBaseWebFetchToolConfig();
|
||||
installWebFetchSsrfHarness();
|
||||
const lookupMock = vi.fn();
|
||||
const baseToolConfig = createBaseWebFetchToolConfig({
|
||||
lookupFn: lookupMock as unknown as LookupFn,
|
||||
});
|
||||
|
||||
function markdownResponse(body: string, extraHeaders: Record<string, string> = {}): Response {
|
||||
return {
|
||||
@@ -34,6 +33,21 @@ function htmlResponse(body: string): Response {
|
||||
}
|
||||
|
||||
describe("web_fetch Cloudflare Markdown for Agents", () => {
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockImplementation(async (hostname: string) => {
|
||||
void hostname;
|
||||
return [{ address: "93.184.216.34", family: 4 }];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("sends Accept header preferring text/markdown", async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(markdownResponse("# Test Page\n\nHello world."));
|
||||
global.fetch = withFetchPreconnect(fetchSpy);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import type { LookupFn } from "../../infra/net/ssrf.js";
|
||||
import { type FetchMock, withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { makeFetchHeaders } from "./web-fetch.test-harness.js";
|
||||
import "./web-fetch.test-mocks.js";
|
||||
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
|
||||
function redirectResponse(location: string): Response {
|
||||
return {
|
||||
@@ -59,6 +58,7 @@ async function createWebFetchToolForTest(params?: { firecrawlApiKey?: string })
|
||||
},
|
||||
},
|
||||
},
|
||||
lookupFn: lookupMock as unknown as LookupFn,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,16 +75,12 @@ describe("web_fetch SSRF protection", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "");
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||
resolvePinnedHostname(hostname, lookupMock),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockClear();
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("blocks localhost hostnames before fetch/firecrawl", async () => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, beforeEach, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import type { LookupFn } from "../../infra/net/ssrf.js";
|
||||
|
||||
export function makeFetchHeaders(map: Record<string, string>): {
|
||||
get: (key: string) => string | null;
|
||||
@@ -9,26 +8,10 @@ export function makeFetchHeaders(map: Record<string, string>): {
|
||||
};
|
||||
}
|
||||
|
||||
export function installWebFetchSsrfHarness() {
|
||||
const lookupMock = vi.fn();
|
||||
const resolvePinnedHostname = ssrf.resolvePinnedHostname;
|
||||
const priorFetch = global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
lookupMock.mockResolvedValue([{ address: "93.184.216.34", family: 4 }]);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation((hostname) =>
|
||||
resolvePinnedHostname(hostname, lookupMock),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
}
|
||||
|
||||
export function createBaseWebFetchToolConfig(opts?: { maxResponseBytes?: number }): {
|
||||
export function createBaseWebFetchToolConfig(opts?: {
|
||||
maxResponseBytes?: number;
|
||||
lookupFn?: LookupFn;
|
||||
}): {
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
@@ -40,6 +23,7 @@ export function createBaseWebFetchToolConfig(opts?: { maxResponseBytes?: number
|
||||
};
|
||||
};
|
||||
};
|
||||
lookupFn?: LookupFn;
|
||||
} {
|
||||
return {
|
||||
config: {
|
||||
@@ -53,5 +37,6 @@ export function createBaseWebFetchToolConfig(opts?: { maxResponseBytes?: number
|
||||
},
|
||||
},
|
||||
},
|
||||
...(opts?.lookupFn ? { lookupFn: opts.lookupFn } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { SsrFBlockedError } from "../../infra/net/ssrf.js";
|
||||
import { SsrFBlockedError, type LookupFn } from "../../infra/net/ssrf.js";
|
||||
import { logDebug } from "../../logger.js";
|
||||
import type { RuntimeWebFetchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import { wrapExternalContent, wrapWebContent } from "../../security/external-content.js";
|
||||
@@ -244,6 +244,7 @@ type WebFetchRuntimeParams = {
|
||||
cacheTtlMs: number;
|
||||
userAgent: string;
|
||||
readabilityEnabled: boolean;
|
||||
lookupFn?: LookupFn;
|
||||
resolveProviderFallback: () => ReturnType<typeof resolveWebFetchDefinition>;
|
||||
};
|
||||
|
||||
@@ -390,6 +391,7 @@ async function runWebFetch(params: WebFetchRuntimeParams): Promise<Record<string
|
||||
url: params.url,
|
||||
maxRedirects: params.maxRedirects,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
lookupFn: params.lookupFn,
|
||||
init: {
|
||||
headers: {
|
||||
Accept: "text/markdown, text/html;q=0.9, */*;q=0.1",
|
||||
@@ -568,6 +570,7 @@ export function createWebFetchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
sandboxed?: boolean;
|
||||
runtimeWebFetch?: RuntimeWebFetchMetadata;
|
||||
lookupFn?: LookupFn;
|
||||
}): AnyAgentTool | null {
|
||||
const fetch = resolveFetchConfig(options?.config);
|
||||
if (!resolveFetchEnabled({ fetch, sandboxed: options?.sandboxed })) {
|
||||
@@ -618,6 +621,7 @@ export function createWebFetchTool(options?: {
|
||||
cacheTtlMs: resolveCacheTtlMs(fetch?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
|
||||
userAgent,
|
||||
readabilityEnabled,
|
||||
lookupFn: options?.lookupFn,
|
||||
resolveProviderFallback,
|
||||
});
|
||||
return jsonResult(result);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EnvHttpProxyAgent } from "undici";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as ssrf from "../../infra/net/ssrf.js";
|
||||
import type { LookupFn } from "../../infra/net/ssrf.js";
|
||||
import { resolveRequestUrl } from "../../plugin-sdk/request-url.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import { makeFetchHeaders } from "./web-fetch.test-harness.js";
|
||||
@@ -22,6 +22,8 @@ vi.mock("../../web-fetch/runtime.js", () => ({
|
||||
}));
|
||||
import { createWebFetchTool } from "./web-tools.js";
|
||||
|
||||
const lookupMock = vi.fn();
|
||||
|
||||
type MockResponse = {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
@@ -92,6 +94,7 @@ function createFetchTool(fetchOverrides: Record<string, unknown> = {}) {
|
||||
},
|
||||
},
|
||||
sandboxed: false,
|
||||
lookupFn: lookupMock as unknown as LookupFn,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,19 +146,18 @@ describe("web_fetch extraction fallbacks", () => {
|
||||
extractReadableContentMock.mockResolvedValue(null);
|
||||
resolveWebFetchDefinitionMock.mockReset();
|
||||
resolveWebFetchDefinitionMock.mockReturnValue(null);
|
||||
vi.spyOn(ssrf, "resolvePinnedHostname").mockImplementation(async (hostname) => {
|
||||
const normalized = hostname.trim().toLowerCase().replace(/\.$/, "");
|
||||
const addresses = ["93.184.216.34", "93.184.216.35"];
|
||||
return {
|
||||
hostname: normalized,
|
||||
addresses,
|
||||
lookup: ssrf.createPinnedLookup({ hostname: normalized, addresses }),
|
||||
};
|
||||
lookupMock.mockImplementation(async (hostname: string) => {
|
||||
void hostname;
|
||||
return [
|
||||
{ address: "93.184.216.34", family: 4 },
|
||||
{ address: "93.184.216.35", family: 4 },
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = priorFetch;
|
||||
lookupMock.mockReset();
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user