mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 22:12:32 +08:00
fix(extensions): guard channel runtime fetches
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { GoogleAuth, OAuth2Client } from "google-auth-library";
|
||||
import { fetchWithSsrFGuard } from "../runtime-api.js";
|
||||
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
||||
|
||||
const CHAT_SCOPE = "https://www.googleapis.com/auth/chat.bot";
|
||||
@@ -83,13 +84,20 @@ async function fetchChatCerts(): Promise<Record<string, string>> {
|
||||
if (cachedCerts && now - cachedCerts.fetchedAt < 10 * 60 * 1000) {
|
||||
return cachedCerts.certs;
|
||||
}
|
||||
const res = await fetch(CHAT_CERTS_URL);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch Chat certs (${res.status})`);
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: CHAT_CERTS_URL,
|
||||
auditContext: "googlechat.auth.certs",
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch Chat certs (${response.status})`);
|
||||
}
|
||||
const certs = (await response.json()) as Record<string, string>;
|
||||
cachedCerts = { fetchedAt: now, certs };
|
||||
return certs;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
const certs = (await res.json()) as Record<string, string>;
|
||||
cachedCerts = { fetchedAt: now, certs };
|
||||
return certs;
|
||||
}
|
||||
|
||||
export type GoogleChatAudienceType = "app-url" | "project-number";
|
||||
|
||||
@@ -14,6 +14,7 @@ const mocks = vi.hoisted(() => ({
|
||||
response: await fetch(params.url, params.init),
|
||||
release: async () => {},
|
||||
})),
|
||||
verifySignedJwtWithCertsAsync: vi.fn(),
|
||||
verifyIdToken: vi.fn(),
|
||||
getGoogleChatAccessToken: vi.fn().mockResolvedValue("token"),
|
||||
}));
|
||||
@@ -28,6 +29,7 @@ vi.mock("google-auth-library", () => ({
|
||||
GoogleAuth: function GoogleAuth() {},
|
||||
OAuth2Client: class {
|
||||
verifyIdToken = mocks.verifyIdToken;
|
||||
verifySignedJwtWithCertsAsync = mocks.verifySignedJwtWithCertsAsync;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -293,4 +295,34 @@ describe("verifyGoogleChatRequest", () => {
|
||||
reason: "unexpected add-on principal: principal-2",
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches Chat certs through the guarded fetch for project-number tokens", async () => {
|
||||
const release = vi.fn();
|
||||
mocks.fetchWithSsrFGuard.mockClear();
|
||||
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
||||
response: new Response(JSON.stringify({ "kid-1": "cert-body" }), { status: 200 }),
|
||||
release,
|
||||
});
|
||||
mocks.verifySignedJwtWithCertsAsync.mockReset().mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
verifyGoogleChatRequest({
|
||||
bearer: "token",
|
||||
audienceType: "project-number",
|
||||
audience: "123456789",
|
||||
}),
|
||||
).resolves.toEqual({ ok: true });
|
||||
|
||||
expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
|
||||
url: "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com",
|
||||
auditContext: "googlechat.auth.certs",
|
||||
});
|
||||
expect(mocks.verifySignedJwtWithCertsAsync).toHaveBeenCalledWith(
|
||||
"token",
|
||||
{ "kid-1": "cert-body" },
|
||||
"123456789",
|
||||
["chat@system.gserviceaccount.com"],
|
||||
);
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,15 @@ function clearTestUndiciRuntimeDepsOverride(): void {
|
||||
Reflect.deleteProperty(globalThis as object, TEST_UNDICI_RUNTIME_DEPS_KEY);
|
||||
}
|
||||
|
||||
function stubRuntimeFetch(fetchImpl: typeof fetch): void {
|
||||
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
||||
Agent: function MockAgent() {},
|
||||
EnvHttpProxyAgent: function MockEnvHttpProxyAgent() {},
|
||||
ProxyAgent: function MockProxyAgent() {},
|
||||
fetch: fetchImpl,
|
||||
};
|
||||
}
|
||||
|
||||
describe("performMatrixRequest", () => {
|
||||
beforeEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
@@ -19,8 +28,7 @@ describe("performMatrixRequest", () => {
|
||||
});
|
||||
|
||||
it("rejects oversized raw responses before buffering the whole body", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
stubRuntimeFetch(
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response("too-big", {
|
||||
@@ -55,8 +63,7 @@ describe("performMatrixRequest", () => {
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
stubRuntimeFetch(
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(stream, {
|
||||
@@ -87,8 +94,7 @@ describe("performMatrixRequest", () => {
|
||||
controller.enqueue(new Uint8Array([1, 2, 3]));
|
||||
},
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
stubRuntimeFetch(
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(stream, {
|
||||
@@ -135,12 +141,7 @@ describe("performMatrixRequest", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
||||
Agent: function MockAgent() {},
|
||||
EnvHttpProxyAgent: function MockEnvHttpProxyAgent() {},
|
||||
ProxyAgent: function MockProxyAgent() {},
|
||||
fetch: runtimeFetch,
|
||||
};
|
||||
stubRuntimeFetch(runtimeFetch);
|
||||
|
||||
const result = await performMatrixRequest({
|
||||
homeserver: "http://127.0.0.1:8008",
|
||||
|
||||
@@ -89,13 +89,6 @@ function buildBufferedResponse(params: {
|
||||
return response;
|
||||
}
|
||||
|
||||
function isMockedFetch(fetchImpl: typeof fetch | undefined): boolean {
|
||||
if (typeof fetchImpl !== "function") {
|
||||
return false;
|
||||
}
|
||||
return typeof (fetchImpl as typeof fetch & { mock?: unknown }).mock === "object";
|
||||
}
|
||||
|
||||
async function fetchWithMatrixDispatcher(params: {
|
||||
url: string;
|
||||
init: MatrixDispatcherRequestInit;
|
||||
@@ -104,10 +97,7 @@ async function fetchWithMatrixDispatcher(params: {
|
||||
// fetches must stay fail-closed unless a retry path can preserve the
|
||||
// validated pinned-address binding. Route dispatcher-attached requests
|
||||
// through undici runtime fetch so the pinned dispatcher is preserved.
|
||||
if (params.init.dispatcher && !isMockedFetch(globalThis.fetch)) {
|
||||
return await fetchWithRuntimeDispatcher(params.url, params.init);
|
||||
}
|
||||
return await fetch(params.url, params.init);
|
||||
return await fetchWithRuntimeDispatcher(params.url, params.init);
|
||||
}
|
||||
|
||||
async function fetchWithMatrixGuardedRedirects(params: {
|
||||
|
||||
Reference in New Issue
Block a user