fix(google-meet): guard node-host meet URLs (#104687)

This commit is contained in:
llagy007
2026-07-12 13:55:38 +08:00
committed by GitHub
parent 4b50658b76
commit f6d11d0e29
5 changed files with 62 additions and 25 deletions

View File

@@ -63,6 +63,33 @@ describe("google-meet node host bridge sessions", () => {
);
});
it("rejects non-Meet start URLs before local Chrome side effects", async () => {
const originalPlatform = process.platform;
children.length = 0;
vi.mocked(spawnSync).mockClear();
Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
try {
await expect(
handleGoogleMeetNodeHostCommand(
JSON.stringify({
action: "start",
url: "https://example.com/private-call",
mode: "realtime",
launch: true,
audioInputCommand: ["mock-rec"],
audioOutputCommand: ["mock-play"],
}),
),
).rejects.toThrow("url must be an explicit https://meet.google.com/... URL");
expect(spawnSync).not.toHaveBeenCalled();
expect(children).toHaveLength(0);
} finally {
Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
}
});
it("starts observe-only Chrome without BlackHole or bridge processes", async () => {
const originalPlatform = process.platform;
children.length = 0;

View File

@@ -0,0 +1,29 @@
// Google Meet plugin module implements shared Meet URL contracts.
function normalizeOptionalString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
}
export function normalizeMeetUrl(input: unknown): string {
const raw = normalizeOptionalString(input);
if (!raw) {
throw new Error("url required");
}
let url: URL;
try {
url = new URL(raw);
} catch {
throw new Error("url must be a valid Google Meet URL");
}
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "meet.google.com") {
throw new Error("url must be an explicit https://meet.google.com/... URL");
}
if (!/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i.test(url.pathname)) {
throw new Error("url must include a Google Meet meeting code");
}
return url.toString();
}

View File

@@ -6,6 +6,7 @@ import {
DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
} from "./config.js";
import { normalizeMeetUrl } from "./meet-url.js";
import {
GOOGLE_MEET_SYSTEM_PROFILER_COMMAND,
outputMentionsBlackHole2ch,
@@ -278,10 +279,7 @@ function clearAudio(params: Record<string, unknown>) {
}
function startChrome(params: Record<string, unknown>) {
const url = readString(params.url);
if (!url) {
throw new Error("url required");
}
const url = normalizeMeetUrl(params.url);
const timeoutMs = readNumber(params.joinTimeoutMs, 30_000);
const mode = readString(params.mode);

View File

@@ -4,7 +4,7 @@ import type {
OpenClawPluginNodeInvokePolicyResult,
} from "openclaw/plugin-sdk/plugin-entry";
import type { GoogleMeetConfig } from "./config.js";
import { normalizeMeetUrl } from "./runtime.js";
import { normalizeMeetUrl } from "./meet-url.js";
export const GOOGLE_MEET_CHROME_NODE_COMMAND = "googlemeet.chrome";

View File

@@ -12,6 +12,7 @@ import type {
GoogleMeetModeInput,
GoogleMeetTransport,
} from "./config.js";
import { normalizeMeetUrl } from "./meet-url.js";
import { addGoogleMeetSetupCheck, getGoogleMeetSetupStatus } from "./setup.js";
import {
isSameMeetUrlForReuse,
@@ -50,6 +51,8 @@ import {
type VoiceCallGateway,
} from "./voice-call-gateway.js";
export { normalizeMeetUrl } from "./meet-url.js";
type ChromeAudioBridgeResult = NonNullable<
| Awaited<ReturnType<typeof launchChromeMeet>>["audioBridge"]
| Awaited<ReturnType<typeof launchChromeMeetOnNode>>["audioBridge"]
@@ -78,26 +81,6 @@ function buildTwilioVoiceCallSessionKey(meetingSessionId: string): string {
return `voice:google-meet:${meetingSessionId}`;
}
export function normalizeMeetUrl(input: unknown): string {
const raw = normalizeOptionalString(input);
if (!raw) {
throw new Error("url required");
}
let url: URL;
try {
url = new URL(raw);
} catch {
throw new Error("url must be a valid Google Meet URL");
}
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "meet.google.com") {
throw new Error("url must be an explicit https://meet.google.com/... URL");
}
if (!/^\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i.test(url.pathname)) {
throw new Error("url must include a Google Meet meeting code");
}
return url.toString();
}
function resolveTransport(input: GoogleMeetTransport | undefined, config: GoogleMeetConfig) {
return input ?? config.defaultTransport;
}