Files
browser/src/cdp/browser.zig
Karl Seguin a3e2b5246e Make CDP server more authoritative with respect to IDs
The TL;DR is that this commit enforces the use of correct IDs, introduces a
BrowserContext, and adds some CDP tests.

These are the ids we need to be aware of when talking about CDP:
- id
- browserContextId
- targetId
- sessionId
- loaderId
- frameId

The `id` is the only one that _should_ originate from the driver. It's attached
to most messages and it's how we maintain a request -> response flow: when
the server responds to a specific message, it echo's back the id from the
requested message. (As opposed to out-of-band events sent from the server which
won't have an `id`). When I say "id" from this point forward, I mean every id
except for this req->res id.

Every other id is created by the browser.

Prior to this commit, we didn't really check incoming ids from the driver. If
the driver said "attachToTarget" and included a targetId, we just assumed that
this was the current targetId. This was aided by the fact that we only used
hard-coded IDS. If _we_ only "create" a frameId of "FRAME-1", then it's tempting
to think the driver will only ever send a frameId of "FRAME-1".

The issue with this approach is that _if_ the browser and driver fall out of sync
and there's only ever 1 browserContextId, 1 sessionId and 1 frameId, it's not
impossible to imagine cases where we behave on the thing.

Imagine this flow:
- Driver asks for a new BrowserContext
- Browser says OK, your browserContextId is 1
- Driver, for whatever reason, says close browserContextId 2
- Browser says, OK, but it doesn't check the id and just closes the only
  BrowserContext it knows about (which is 1)

By both re-using the same hard-coded ids, and not verifying that the ids sent
from the client correspond to the correct ids, any issues are going to be hard
to debug.

Currently LOADER_ID and FRAEM_ID are still hard-coded. Baby steps.
2025-03-10 14:34:32 +01:00

119 lines
3.7 KiB
Zig

// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const cdp = @import("cdp.zig");
// TODO: hard coded data
const PROTOCOL_VERSION = "1.3";
const PRODUCT = "Chrome/124.0.6367.29";
const REVISION = "@9e6ded5ac1ff5e38d930ae52bd9aec09bd1a68e4";
const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const JS_VERSION = "12.4.254.8";
const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setDownloadBehavior,
getWindowForTarget,
setWindowBounds,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.getVersion => return getVersion(cmd),
.setDownloadBehavior => return setDownloadBehavior(cmd),
.getWindowForTarget => return getWindowForTarget(cmd),
.setWindowBounds => return setWindowBounds(cmd),
}
}
fn getVersion(cmd: anytype) !void {
// TODO: pre-serialize?
return cmd.sendResult(.{
.protocolVersion = PROTOCOL_VERSION,
.product = PRODUCT,
.revision = REVISION,
.userAgent = USER_AGENT,
.jsVersion = JS_VERSION,
}, .{ .include_session_id = false });
}
// TODO: noop method
fn setDownloadBehavior(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// behavior: []const u8,
// browserContextId: ?[]const u8 = null,
// downloadPath: ?[]const u8 = null,
// eventsEnabled: ?bool = null,
// })) orelse return error.InvalidParams;
return cmd.sendResult(null, .{ .include_session_id = false });
}
fn getWindowForTarget(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams;
return cmd.sendResult(.{ .windowId = DEV_TOOLS_WINDOW_ID, .bounds = .{
.windowState = "normal",
} }, .{});
}
// TODO: noop method
fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
const testing = @import("testing.zig");
test "cdp.browser: getVersion" {
var ctx = testing.context();
defer ctx.deinit();
try ctx.processMessage(.{
.id = 32,
.method = "Browser.getVersion",
});
try ctx.expectSentCount(1);
try ctx.expectSentResult(.{
.protocolVersion = PROTOCOL_VERSION,
.product = PRODUCT,
.revision = REVISION,
.userAgent = USER_AGENT,
.jsVersion = JS_VERSION,
}, .{ .id = 32, .index = 0, .session_id = null });
}
test "cdp.browser: getWindowForTarget" {
var ctx = testing.context();
defer ctx.deinit();
try ctx.processMessage(.{
.id = 33,
.method = "Browser.getWindowForTarget",
});
try ctx.expectSentCount(1);
try ctx.expectSentResult(.{
.windowId = DEV_TOOLS_WINDOW_ID,
.bounds = .{ .windowState = "normal" },
}, .{ .id = 33, .index = 0, .session_id = null });
}