Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/lib/browser-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export class BrowserRouteCache {
}

const BROWSER_ROUTING_SUBRESOURCES_ENV = 'KERNEL_BROWSER_ROUTING_SUBRESOURCES';
const DEFAULT_BROWSER_ROUTING_SUBRESOURCES = ['curl', 'telemetry'];
// Path prefixes eligible for direct-to-VM routing. "telemetry/stream" is the live
// SSE endpoint (served by the VM); "telemetry/events" is a historical read served
// by the control plane (S2) and must NOT be here.
const DEFAULT_BROWSER_ROUTING_SUBRESOURCES = ['curl', 'telemetry/stream'];
const BROWSER_ROUTE_CACHEABLE_PATH = /^\/(?:v\d+\/)?browsers(?:\/[^/]+)?\/?$/;
const BROWSER_POOL_ACQUIRE_PATH = /^\/(?:v\d+\/)?browser_pools\/[^/]+\/acquire\/?$/;
const BROWSER_DELETE_BY_ID_PATH = /^\/(?:v\d+\/)?browsers\/([^/]+)\/?$/;
Expand Down Expand Up @@ -62,7 +65,7 @@ export function createRoutingFetch(
cache: BrowserRouteCache;
},
): Fetch {
const allowed = new Set([...subresources].map((value) => value.trim()).filter(Boolean));
const allowed = [...subresources].map((value) => value.trim().replace(/^\/+|\/+$/g, '')).filter(Boolean);
const apiOrigin = new URL(apiBaseURL).origin;

return async (input, init) => {
Expand Down Expand Up @@ -202,6 +205,16 @@ function populateCache(value: unknown, cache: BrowserRouteCache): void {
}
}

// matchesDirectVMPrefix reports whether tail (the path after browsers/{id}/) is
// covered by an allow prefix, matching on segment boundaries: "telemetry/stream"
// matches "telemetry/stream" and "telemetry/stream/...", but not "telemetry/events"
// or "telemetry/streamfoo". Keeps historical control-plane reads (telemetry/events,
// served from S2) off the VM.
export function matchesDirectVMPrefix(tail: string, prefixes: readonly string[]): boolean {
const t = tail.replace(/^\/+|\/+$/g, '');
return prefixes.some((p) => t === p || t.startsWith(p + '/'));
}

async function routeRequest(
innerFetch: Fetch,
{
Expand All @@ -214,7 +227,7 @@ async function routeRequest(
request: Request;
},
apiOrigin: string,
allowed: ReadonlySet<string>,
allowed: readonly string[],
cache: BrowserRouteCache,
): Promise<Response> {
const url = new URL(request.url);
Expand All @@ -229,7 +242,7 @@ async function routeRequest(

const sessionId = decodeURIComponent(match[1] ?? '');
const subresource = match[2] ?? '';
if (!sessionId || !allowed.has(subresource)) {
if (!sessionId || !matchesDirectVMPrefix(subresource + (match[3] ?? ''), allowed)) {
return innerFetch(input, init);
}
const route = cache.get(sessionId);
Expand Down
16 changes: 14 additions & 2 deletions tests/lib/browser-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BrowserRouteCache,
browserRoutingSubresourcesFromEnv,
createRoutingFetch,
matchesDirectVMPrefix,
} from '../../src/lib/browser-routing';

describe('browser routing', () => {
Expand Down Expand Up @@ -381,12 +382,23 @@ describe('browser routing', () => {
).rejects.toThrow(/unsupported HTTP method/i);
});

test('defaults browser routing subresources to curl and telemetry when env is unset', async () => {
test('defaults browser routing subresources to curl and telemetry/stream when env is unset', async () => {
await withBrowserRoutingEnv(undefined, async () => {
expect(browserRoutingSubresourcesFromEnv()).toEqual(['curl', 'telemetry']);
expect(browserRoutingSubresourcesFromEnv()).toEqual(['curl', 'telemetry/stream']);
});
});

test('allowlist matching is segment-boundary aware (telemetry/events stays on the control plane)', () => {
const prefixes = ['curl', 'telemetry/stream'];
expect(matchesDirectVMPrefix('telemetry/stream', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('telemetry/stream/x', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('telemetry/events', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('telemetry/streaming-config', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('telemetry', prefixes)).toBe(false);
expect(matchesDirectVMPrefix('curl/raw', prefixes)).toBe(true);
expect(matchesDirectVMPrefix('fs/read', prefixes)).toBe(false);
});

test('routes telemetry stream calls to the VM /telemetry/stream path by default', async () => {
await withBrowserRoutingEnv(undefined, async () => {
const calls: Array<{ url: string; headers: Headers }> = [];
Expand Down
Loading