mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:00:42 +00:00
fix: guard Google Meet API fetches
This commit is contained in:
@@ -24,6 +24,25 @@ const voiceCallMocks = vi.hoisted(() => ({
|
||||
joinMeetViaVoiceCallGateway: vi.fn(async () => ({ callId: "call-1", dtmfSent: true })),
|
||||
}));
|
||||
|
||||
const fetchGuardMocks = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuard: vi.fn(
|
||||
async (params: {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
}): Promise<{
|
||||
response: Response;
|
||||
release: () => Promise<void>;
|
||||
}> => ({
|
||||
response: await fetch(params.url, params.init),
|
||||
release: vi.fn(async () => {}),
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard,
|
||||
}));
|
||||
|
||||
vi.mock("./src/voice-call-gateway.js", () => ({
|
||||
joinMeetViaVoiceCallGateway: voiceCallMocks.joinMeetViaVoiceCallGateway,
|
||||
}));
|
||||
@@ -179,6 +198,16 @@ describe("google-meet plugin", () => {
|
||||
meeting: "spaces/abc-defg-hij",
|
||||
}),
|
||||
).resolves.toMatchObject({ name: "spaces/abc-defg-hij" });
|
||||
expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://meet.googleapis.com/v2/spaces/abc-defg-hij",
|
||||
init: expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: "Bearer token" }),
|
||||
}),
|
||||
policy: { allowedHostnames: ["meet.googleapis.com"] },
|
||||
auditContext: "google-meet.spaces.get",
|
||||
}),
|
||||
);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://meet.googleapis.com/v2/spaces/abc-defg-hij",
|
||||
expect.objectContaining({
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
const GOOGLE_MEET_API_BASE_URL = "https://meet.googleapis.com/v2";
|
||||
const GOOGLE_MEET_URL_HOST = "meet.google.com";
|
||||
const GOOGLE_MEET_API_HOST = "meet.googleapis.com";
|
||||
|
||||
export type GoogleMeetSpace = {
|
||||
name: string;
|
||||
@@ -58,21 +61,30 @@ export async function fetchGoogleMeetSpace(params: {
|
||||
meeting: string;
|
||||
}): Promise<GoogleMeetSpace> {
|
||||
const name = normalizeGoogleMeetSpaceName(params.meeting);
|
||||
const response = await fetch(`${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(name)}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: "application/json",
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: `${GOOGLE_MEET_API_BASE_URL}/${encodeSpaceNameForPath(name)}`,
|
||||
init: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
|
||||
auditContext: "google-meet.spaces.get",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(`Google Meet spaces.get failed (${response.status}): ${detail}`);
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(`Google Meet spaces.get failed (${response.status}): ${detail}`);
|
||||
}
|
||||
const payload = (await response.json()) as GoogleMeetSpace;
|
||||
if (!payload.name?.trim()) {
|
||||
throw new Error("Google Meet spaces.get response was missing name");
|
||||
}
|
||||
return payload;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
const payload = (await response.json()) as GoogleMeetSpace;
|
||||
if (!payload.name?.trim()) {
|
||||
throw new Error("Google Meet spaces.get response was missing name");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function buildGoogleMeetPreflightReport(params: {
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
parseOAuthCallbackInput,
|
||||
waitForLocalOAuthCallback,
|
||||
} from "openclaw/plugin-sdk/provider-auth-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
export const GOOGLE_MEET_REDIRECT_URI = "http://localhost:8085/oauth2callback";
|
||||
export const GOOGLE_MEET_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
export const GOOGLE_MEET_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
const GOOGLE_MEET_TOKEN_HOST = "oauth2.googleapis.com";
|
||||
export const GOOGLE_MEET_SCOPES = [
|
||||
"https://www.googleapis.com/auth/meetings.space.readonly",
|
||||
"https://www.googleapis.com/auth/meetings.conference.media.readonly",
|
||||
@@ -43,40 +45,49 @@ export function buildGoogleMeetAuthUrl(params: {
|
||||
}
|
||||
|
||||
async function executeGoogleTokenRequest(body: URLSearchParams): Promise<GoogleMeetOAuthTokens> {
|
||||
const response = await fetch(GOOGLE_MEET_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: GOOGLE_MEET_TOKEN_URL,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body,
|
||||
},
|
||||
body,
|
||||
policy: { allowedHostnames: [GOOGLE_MEET_TOKEN_HOST] },
|
||||
auditContext: "google-meet.oauth.token",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(`Google OAuth token request failed (${response.status}): ${detail}`);
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(`Google OAuth token request failed (${response.status}): ${detail}`);
|
||||
}
|
||||
const payload = (await response.json()) as {
|
||||
access_token?: string;
|
||||
expires_in?: number;
|
||||
refresh_token?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
};
|
||||
const accessToken = payload.access_token?.trim();
|
||||
if (!accessToken) {
|
||||
throw new Error("Google OAuth token response was missing access_token");
|
||||
}
|
||||
const expiresInSeconds =
|
||||
typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in)
|
||||
? payload.expires_in
|
||||
: 3600;
|
||||
return {
|
||||
accessToken,
|
||||
expiresAt: Date.now() + expiresInSeconds * 1000,
|
||||
refreshToken: payload.refresh_token?.trim() || undefined,
|
||||
scope: payload.scope?.trim() || undefined,
|
||||
tokenType: payload.token_type?.trim() || undefined,
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
const payload = (await response.json()) as {
|
||||
access_token?: string;
|
||||
expires_in?: number;
|
||||
refresh_token?: string;
|
||||
scope?: string;
|
||||
token_type?: string;
|
||||
};
|
||||
const accessToken = payload.access_token?.trim();
|
||||
if (!accessToken) {
|
||||
throw new Error("Google OAuth token response was missing access_token");
|
||||
}
|
||||
const expiresInSeconds =
|
||||
typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in)
|
||||
? payload.expires_in
|
||||
: 3600;
|
||||
return {
|
||||
accessToken,
|
||||
expiresAt: Date.now() + expiresInSeconds * 1000,
|
||||
refreshToken: payload.refresh_token?.trim() || undefined,
|
||||
scope: payload.scope?.trim() || undefined,
|
||||
tokenType: payload.token_type?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenRequestBody(values: Record<string, string | undefined>): URLSearchParams {
|
||||
|
||||
Reference in New Issue
Block a user