From 7d9ae1654e99b8e7b51516c13fa95f150de09c46 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 10:40:26 +0200
Subject: [PATCH 01/14] deleteCookies
---
src/browser/storage/cookie.zig | 147 +++++++++++++++++----------------
src/cdp/domains/network.zig | 48 +++++++++++
2 files changed, 125 insertions(+), 70 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index bdd93e3f7..1b565046b 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -32,6 +32,13 @@ pub const Jar = struct {
self.cookies.deinit(self.allocator);
}
+ pub fn clear(self: *Jar) void {
+ for (self.cookies.items) |c| {
+ c.deinit();
+ }
+ self.cookies.clearRetainingCapacity();
+ }
+
pub fn add(
self: *Jar,
cookie: Cookie,
@@ -173,43 +180,43 @@ pub const Jar = struct {
}
};
-pub const CookieList = struct {
- _cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
-
- pub fn deinit(self: *CookieList, allocator: Allocator) void {
- self._cookies.deinit(allocator);
- }
-
- pub fn cookies(self: *const CookieList) []*const Cookie {
- return self._cookies.items;
- }
-
- pub fn len(self: *const CookieList) usize {
- return self._cookies.items.len;
- }
-
- pub fn write(self: *const CookieList, writer: anytype) !void {
- const all = self._cookies.items;
- if (all.len == 0) {
- return;
- }
- try writeCookie(all[0], writer);
- for (all[1..]) |cookie| {
- try writer.writeAll("; ");
- try writeCookie(cookie, writer);
- }
- }
-
- fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
- if (cookie.name.len > 0) {
- try writer.writeAll(cookie.name);
- try writer.writeByte('=');
- }
- if (cookie.value.len > 0) {
- try writer.writeAll(cookie.value);
- }
- }
-};
+// pub const CookieList = struct {
+// _cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
+
+// pub fn deinit(self: *CookieList, allocator: Allocator) void {
+// self._cookies.deinit(allocator);
+// }
+
+// pub fn cookies(self: *const CookieList) []*const Cookie {
+// return self._cookies.items;
+// }
+
+// pub fn len(self: *const CookieList) usize {
+// return self._cookies.items.len;
+// }
+
+// pub fn write(self: *const CookieList, writer: anytype) !void {
+// const all = self._cookies.items;
+// if (all.len == 0) {
+// return;
+// }
+// try writeCookie(all[0], writer);
+// for (all[1..]) |cookie| {
+// try writer.writeAll("; ");
+// try writeCookie(cookie, writer);
+// }
+// }
+
+// fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
+// if (cookie.name.len > 0) {
+// try writer.writeAll(cookie.name);
+// try writer.writeByte('=');
+// }
+// if (cookie.value.len > 0) {
+// try writer.writeAll(cookie.value);
+// }
+// }
+// };
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
const ce = cookie.expires orelse return false;
@@ -660,39 +667,39 @@ test "Jar: forRequest" {
// the 'global2' cookie
}
-test "CookieList: write" {
- var arr: std.ArrayListUnmanaged(u8) = .{};
- defer arr.deinit(testing.allocator);
-
- var cookie_list = CookieList{};
- defer cookie_list.deinit(testing.allocator);
-
- const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
- defer c1.deinit();
- {
- try cookie_list._cookies.append(testing.allocator, &c1);
- try cookie_list.write(arr.writer(testing.allocator));
- try testing.expectEqual("cookie_name=cookie_value", arr.items);
- }
-
- const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
- defer c2.deinit();
- {
- arr.clearRetainingCapacity();
- try cookie_list._cookies.append(testing.allocator, &c2);
- try cookie_list.write(arr.writer(testing.allocator));
- try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
- }
-
- const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
- defer c3.deinit();
- {
- arr.clearRetainingCapacity();
- try cookie_list._cookies.append(testing.allocator, &c3);
- try cookie_list.write(arr.writer(testing.allocator));
- try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
- }
-}
+// test "CookieList: write" {
+// var arr: std.ArrayListUnmanaged(u8) = .{};
+// defer arr.deinit(testing.allocator);
+
+// var cookie_list = CookieList{};
+// defer cookie_list.deinit(testing.allocator);
+
+// const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
+// defer c1.deinit();
+// {
+// try cookie_list._cookies.append(testing.allocator, &c1);
+// try cookie_list.write(arr.writer(testing.allocator));
+// try testing.expectEqual("cookie_name=cookie_value", arr.items);
+// }
+
+// const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
+// defer c2.deinit();
+// {
+// arr.clearRetainingCapacity();
+// try cookie_list._cookies.append(testing.allocator, &c2);
+// try cookie_list.write(arr.writer(testing.allocator));
+// try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
+// }
+
+// const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
+// defer c3.deinit();
+// {
+// arr.clearRetainingCapacity();
+// try cookie_list._cookies.append(testing.allocator, &c3);
+// try cookie_list.write(arr.writer(testing.allocator));
+// try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
+// }
+// }
test "Cookie: parse key=value" {
try expectError(error.Empty, null, "");
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index a161f589c..db3b2392f 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -28,6 +28,7 @@ pub fn processMessage(cmd: anytype) !void {
disable,
setCacheDisabled,
setExtraHTTPHeaders,
+ deleteCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -35,6 +36,7 @@ pub fn processMessage(cmd: anytype) !void {
.disable => return disable(cmd),
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
+ .deleteCookies => return deleteCookies(cmd),
}
}
@@ -71,6 +73,52 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
+// const CookiePartitionKey = struct {
+// topLevelSite: []const u8,
+// hasCrossSiteAncestor: bool,
+// };
+
+const Cookie = @import("../../browser/storage/storage.zig").Cookie;
+const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
+
+fn cookieMatches(cookie: *const Cookie, name: []const u8, url: ?[]const u8, domain: ?[]const u8, path: ?[]const u8) bool {
+ if (!std.mem.eql(u8, cookie.name, name)) return false;
+
+ _ = url; // TODO
+
+ if (domain) |domain_| {
+ if (!std.mem.eql(u8, cookie.domain, domain_)) return false;
+ }
+ if (path) |path_| {
+ if (!std.mem.eql(u8, cookie.path, path_)) return false;
+ }
+
+ return true;
+}
+
+fn deleteCookies(cmd: anytype) !void {
+ const params = (try cmd.params(struct {
+ name: []const u8,
+ url: ?[]const u8 = null,
+ domain: ?[]const u8 = null,
+ path: ?[]const u8 = null,
+ // partitionKey: ?CookiePartitionKey,
+ })) orelse return error.InvalidParams;
+
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ const cookies = &bc.session.cookie_jar.cookies;
+
+ var index = cookies.items.len;
+ while (index > 0) {
+ index -= 1;
+ const cookie = &cookies.items[index];
+ if (cookieMatches(cookie, params.name, params.url, params.domain, params.path)) {
+ cookies.swapRemove(index).deinit();
+ }
+ }
+ return cmd.sendResult(null, .{});
+}
+
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
From 2a43838372ebc8ee225ae3ef4e1689cbc1d1a02a Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 13:54:19 +0200
Subject: [PATCH 02/14] setCookies
---
src/browser/storage/cookie.zig | 8 +--
src/cdp/domains/network.zig | 121 ++++++++++++++++++++++++++++++---
2 files changed, 117 insertions(+), 12 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 1b565046b..c76ba308c 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -266,9 +266,9 @@ pub const Cookie = struct {
path: []const u8,
domain: []const u8,
expires: ?i64,
- secure: bool,
- http_only: bool,
- same_site: SameSite,
+ secure: bool = false,
+ http_only: bool = false,
+ same_site: SameSite = .none,
const SameSite = enum {
strict,
@@ -372,7 +372,7 @@ pub const Cookie = struct {
if (std.mem.endsWith(u8, host, value) == false) {
return error.InvalidDomain;
}
- domain = value;
+ domain = value; // TODO to lower case: https://www.rfc-editor.org/rfc/rfc6265#section-5.2.3
},
.secure => secure = true,
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index db3b2392f..6f563bcbb 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -29,6 +29,7 @@ pub fn processMessage(cmd: anytype) !void {
setCacheDisabled,
setExtraHTTPHeaders,
deleteCookies,
+ setCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -37,6 +38,7 @@ pub fn processMessage(cmd: anytype) !void {
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
+ .setCookies => return setCookies(cmd),
}
}
@@ -73,19 +75,17 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
-// const CookiePartitionKey = struct {
-// topLevelSite: []const u8,
-// hasCrossSiteAncestor: bool,
-// };
+const CookiePartitionKey = struct {
+ topLevelSite: []const u8,
+ hasCrossSiteAncestor: bool,
+};
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
-fn cookieMatches(cookie: *const Cookie, name: []const u8, url: ?[]const u8, domain: ?[]const u8, path: ?[]const u8) bool {
+fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {
if (!std.mem.eql(u8, cookie.name, name)) return false;
- _ = url; // TODO
-
if (domain) |domain_| {
if (!std.mem.eql(u8, cookie.domain, domain_)) return false;
}
@@ -112,13 +112,118 @@ fn deleteCookies(cmd: anytype) !void {
while (index > 0) {
index -= 1;
const cookie = &cookies.items[index];
- if (cookieMatches(cookie, params.name, params.url, params.domain, params.path)) {
+ const domain = try percentEncodedDomain(cmd.arena, params.url, params.domain);
+ // TBD does chrome take the path from the url as default? (unlike setCookies)
+ if (cookieMatches(cookie, params.name, domain, params.path)) {
cookies.swapRemove(index).deinit();
}
}
return cmd.sendResult(null, .{});
}
+const SameSite = enum {
+ Strict,
+ Lax,
+ None,
+};
+const CookiePriority = enum {
+ Low,
+ Medium,
+ High,
+};
+const CookieSourceScheme = enum {
+ Unset,
+ NonSecure,
+ Secure,
+};
+
+fn isHostChar(c: u8) bool {
+ return switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
+ '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
+ ':' => true,
+ '[', ']' => true,
+ else => false,
+ };
+}
+
+// Note: Chrome does not apply rules like removing a leading `.` from the domain.
+fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
+ if (domain) |domain_| {
+ return try allocator.dupe(u8, domain_);
+ } else if (default_url) |url| {
+ const uri = std.Uri.parse(url) catch return error.InvalidParams;
+
+ switch (uri.host orelse return error.InvalidParams) {
+ .raw => |str| {
+ var list = std.ArrayList(u8).init(allocator);
+ try list.ensureTotalCapacity(str.len); // Expect no precents needed
+ try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
+ return list.items; // @memory retains memory used before growing
+ },
+ .percent_encoded => |str| {
+ return try allocator.dupe(u8, str);
+ },
+ }
+ } else return null;
+}
+
+fn setCookies(cmd: anytype) !void {
+ const params = (try cmd.params(struct {
+ cookies: []const struct {
+ name: []const u8,
+ value: []const u8,
+ url: ?[]const u8 = null,
+ domain: ?[]const u8 = null,
+ path: ?[]const u8 = null,
+ secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
+ expires: ?i64 = null, // -1? says google
+ priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
+ sameParty: ?bool = null,
+ sourceScheme: ?CookieSourceScheme = null,
+ // sourcePort: Temporary ability and it will be removed from CDP
+ partitionKey: ?CookiePartitionKey = null,
+ },
+ })) orelse return error.InvalidParams;
+
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ for (params.cookies) |param| {
+ if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
+ return error.NotYetImplementedParams;
+ }
+ if (param.name.len == 0) return error.InvalidParams;
+ if (param.value.len == 0) return error.InvalidParams;
+
+ var arena = std.heap.ArenaAllocator.init(bc.session.cookie_jar.allocator);
+ errdefer arena.deinit();
+ const a = arena.allocator();
+
+ // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
+ const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
+
+ const cookie = Cookie{
+ .arena = arena,
+ .name = try a.dupe(u8, param.name),
+ .value = try a.dupe(u8, param.value),
+ .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
+ .domain = domain,
+ .expires = param.expires,
+ .secure = param.secure,
+ .http_only = param.httpOnly,
+ .same_site = switch (param.sameSite) {
+ .Strict => .strict,
+ .Lax => .lax,
+ .None => .none,
+ },
+ };
+ try bc.session.cookie_jar.add(cookie, std.time.timestamp());
+ }
+
+ return cmd.sendResult(null, .{});
+}
+
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
From d5a7d0bd952636567573960af8b8a5bbee25a7d6 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 14:05:05 +0200
Subject: [PATCH 03/14] setCookie
---
src/cdp/domains/network.zig | 111 +++++++++++++++++++++---------------
1 file changed, 65 insertions(+), 46 deletions(-)
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 6f563bcbb..7273a83ba 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -29,6 +29,7 @@ pub fn processMessage(cmd: anytype) !void {
setCacheDisabled,
setExtraHTTPHeaders,
deleteCookies,
+ setCookie,
setCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
@@ -38,6 +39,7 @@ pub fn processMessage(cmd: anytype) !void {
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
+ .setCookie => return setCookie(cmd),
.setCookies => return setCookies(cmd),
}
}
@@ -168,60 +170,77 @@ fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain:
} else return null;
}
+const CdpCookie = struct {
+ name: []const u8,
+ value: []const u8,
+ url: ?[]const u8 = null,
+ domain: ?[]const u8 = null,
+ path: ?[]const u8 = null,
+ secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
+ expires: ?i64 = null, // -1? says google
+ priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
+ sameParty: ?bool = null,
+ sourceScheme: ?CookieSourceScheme = null,
+ // sourcePort: Temporary ability and it will be removed from CDP
+ partitionKey: ?CookiePartitionKey = null,
+};
+
+fn setCookie(cmd: anytype) !void {
+ const params = (try cmd.params(
+ CdpCookie,
+ )) orelse return error.InvalidParams;
+
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ try setCdpCookie(&bc.session.cookie_jar, params);
+
+ try cmd.sendResult(.{ .success = true }, .{});
+}
+
fn setCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
- cookies: []const struct {
- name: []const u8,
- value: []const u8,
- url: ?[]const u8 = null,
- domain: ?[]const u8 = null,
- path: ?[]const u8 = null,
- secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
- httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
- sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
- expires: ?i64 = null, // -1? says google
- priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
- sameParty: ?bool = null,
- sourceScheme: ?CookieSourceScheme = null,
- // sourcePort: Temporary ability and it will be removed from CDP
- partitionKey: ?CookiePartitionKey = null,
- },
+ cookies: []const CdpCookie,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
for (params.cookies) |param| {
- if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
- return error.NotYetImplementedParams;
- }
- if (param.name.len == 0) return error.InvalidParams;
- if (param.value.len == 0) return error.InvalidParams;
-
- var arena = std.heap.ArenaAllocator.init(bc.session.cookie_jar.allocator);
- errdefer arena.deinit();
- const a = arena.allocator();
-
- // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
- const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
-
- const cookie = Cookie{
- .arena = arena,
- .name = try a.dupe(u8, param.name),
- .value = try a.dupe(u8, param.value),
- .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
- .domain = domain,
- .expires = param.expires,
- .secure = param.secure,
- .http_only = param.httpOnly,
- .same_site = switch (param.sameSite) {
- .Strict => .strict,
- .Lax => .lax,
- .None => .none,
- },
- };
- try bc.session.cookie_jar.add(cookie, std.time.timestamp());
+ try setCdpCookie(&bc.session.cookie_jar, param);
}
- return cmd.sendResult(null, .{});
+ try cmd.sendResult(null, .{});
+}
+
+fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
+ if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
+ return error.NotYetImplementedParams;
+ }
+ if (param.name.len == 0) return error.InvalidParams;
+ if (param.value.len == 0) return error.InvalidParams;
+
+ var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
+ errdefer arena.deinit();
+ const a = arena.allocator();
+
+ // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
+ const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
+
+ const cookie = Cookie{
+ .arena = arena,
+ .name = try a.dupe(u8, param.name),
+ .value = try a.dupe(u8, param.value),
+ .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
+ .domain = domain,
+ .expires = param.expires,
+ .secure = param.secure,
+ .http_only = param.httpOnly,
+ .same_site = switch (param.sameSite) {
+ .Strict => .strict,
+ .Lax => .lax,
+ .None => .none,
+ },
+ };
+ try cookie_jar.add(cookie, std.time.timestamp());
}
// Upsert a header into the headers array.
From 3bdd39c295401d7dbe26b700c2a6b2340d6efc43 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 15:26:58 +0200
Subject: [PATCH 04/14] clearRetainingCapacity
---
src/browser/storage/cookie.zig | 2 +-
src/cdp/domains/network.zig | 11 +++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index c76ba308c..a73c94481 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -32,7 +32,7 @@ pub const Jar = struct {
self.cookies.deinit(self.allocator);
}
- pub fn clear(self: *Jar) void {
+ pub fn clearRetainingCapacity(self: *Jar) void {
for (self.cookies.items) |c| {
c.deinit();
}
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 7273a83ba..f9754958b 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -29,6 +29,7 @@ pub fn processMessage(cmd: anytype) !void {
setCacheDisabled,
setExtraHTTPHeaders,
deleteCookies,
+ clearBrowserCookies,
setCookie,
setCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
@@ -39,6 +40,7 @@ pub fn processMessage(cmd: anytype) !void {
.setCacheDisabled => return cmd.sendResult(null, .{}),
.setExtraHTTPHeaders => return setExtraHTTPHeaders(cmd),
.deleteCookies => return deleteCookies(cmd),
+ .clearBrowserCookies => return clearBrowserCookies(cmd),
.setCookie => return setCookie(cmd),
.setCookies => return setCookies(cmd),
}
@@ -123,6 +125,15 @@ fn deleteCookies(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
+fn clearBrowserCookies(cmd: anytype) !void {
+ _ = (try cmd.params(struct {})) orelse return error.InvalidParams;
+
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ bc.session.cookie_jar.clearRetainingCapacity();
+
+ return cmd.sendResult(null, .{});
+}
+
const SameSite = enum {
Strict,
Lax,
From f5abedbb64a803db67de84e2f59d70f6bd001d78 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 15:57:14 +0200
Subject: [PATCH 05/14] lower case domain
---
src/browser/storage/cookie.zig | 10 +++++++-
src/cdp/domains/network.zig | 46 ++++++++++++++++++----------------
2 files changed, 34 insertions(+), 22 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index a73c94481..55cbc6c68 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -372,7 +372,7 @@ pub const Cookie = struct {
if (std.mem.endsWith(u8, host, value) == false) {
return error.InvalidDomain;
}
- domain = value; // TODO to lower case: https://www.rfc-editor.org/rfc/rfc6265#section-5.2.3
+ domain = value; // Domain is made lower case after it has relocated to the arena
},
.secure => secure = true,
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
@@ -406,6 +406,7 @@ pub const Cookie = struct {
} else blk: {
break :blk try aa.dupe(u8, host);
};
+ _ = toLower(owned_domain);
var normalized_expires: ?i64 = null;
if (max_age) |ma| {
@@ -470,6 +471,13 @@ fn trimRight(str: []const u8) []const u8 {
return std.mem.trimLeft(u8, str, &std.ascii.whitespace);
}
+pub fn toLower(str: []u8) []u8 {
+ for (str, 0..) |c, i| {
+ str[i] = std.ascii.toLower(c);
+ }
+ return str;
+}
+
const testing = @import("../../testing.zig");
test "cookie: findSecondLevelDomain" {
const cases = [_]struct { []const u8, []const u8 }{
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index f9754958b..55cff2461 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -160,27 +160,6 @@ fn isHostChar(c: u8) bool {
};
}
-// Note: Chrome does not apply rules like removing a leading `.` from the domain.
-fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
- if (domain) |domain_| {
- return try allocator.dupe(u8, domain_);
- } else if (default_url) |url| {
- const uri = std.Uri.parse(url) catch return error.InvalidParams;
-
- switch (uri.host orelse return error.InvalidParams) {
- .raw => |str| {
- var list = std.ArrayList(u8).init(allocator);
- try list.ensureTotalCapacity(str.len); // Expect no precents needed
- try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
- return list.items; // @memory retains memory used before growing
- },
- .percent_encoded => |str| {
- return try allocator.dupe(u8, str);
- },
- }
- } else return null;
-}
-
const CdpCookie = struct {
name: []const u8,
value: []const u8,
@@ -254,6 +233,31 @@ fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
try cookie_jar.add(cookie, std.time.timestamp());
}
+// Note: Chrome does not apply rules like removing a leading `.` from the domain.
+fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
+ const toLower = @import("../../browser/storage/cookie.zig").toLower;
+ if (domain) |domain_| {
+ const output = try allocator.dupe(u8, domain_);
+ return toLower(output);
+ } else if (default_url) |url| {
+ const uri = std.Uri.parse(url) catch return error.InvalidParams;
+
+ var output: []u8 = undefined;
+ switch (uri.host orelse return error.InvalidParams) {
+ .raw => |str| {
+ var list = std.ArrayList(u8).init(allocator);
+ try list.ensureTotalCapacity(str.len); // Expect no precents needed
+ try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
+ output = list.items; // @memory retains memory used before growing
+ },
+ .percent_encoded => |str| {
+ output = try allocator.dupe(u8, str);
+ },
+ }
+ return toLower(output);
+ } else return null;
+}
+
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
From c3cdc15adf6a3fcd6e797a7701a0acaef93b688d Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Wed, 11 Jun 2025 17:30:19 +0200
Subject: [PATCH 06/14] storage cookies
---
src/browser/storage/cookie.zig | 2 +-
src/cdp/cdp.zig | 1 +
src/cdp/domains/network.zig | 121 ++--------------
src/cdp/domains/storage.zig | 255 +++++++++++++++++++++++++++++++++
4 files changed, 265 insertions(+), 114 deletions(-)
create mode 100644 src/cdp/domains/storage.zig
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 55cbc6c68..aa93ba21b 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -263,8 +263,8 @@ pub const Cookie = struct {
arena: ArenaAllocator,
name: []const u8,
value: []const u8,
- path: []const u8,
domain: []const u8,
+ path: []const u8,
expires: ?i64,
secure: bool = false,
http_only: bool = false,
diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig
index 3b0005f4a..332c670c8 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -204,6 +204,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
asUint("Browser") => return @import("domains/browser.zig").processMessage(command),
asUint("Runtime") => return @import("domains/runtime.zig").processMessage(command),
asUint("Network") => return @import("domains/network.zig").processMessage(command),
+ asUint("Storage") => return @import("domains/storage.zig").processMessage(command),
else => {},
},
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 55cff2461..c7ad0c609 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -17,10 +17,11 @@
// along with this program. If not, see .
const std = @import("std");
+const Allocator = std.mem.Allocator;
+
const Notification = @import("../../notification.zig").Notification;
const log = @import("../../log.zig");
-
-const Allocator = std.mem.Allocator;
+const CdpStorage = @import("storage.zig");
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -79,13 +80,7 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
-const CookiePartitionKey = struct {
- topLevelSite: []const u8,
- hasCrossSiteAncestor: bool,
-};
-
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
-const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {
if (!std.mem.eql(u8, cookie.name, name)) return false;
@@ -116,7 +111,7 @@ fn deleteCookies(cmd: anytype) !void {
while (index > 0) {
index -= 1;
const cookie = &cookies.items[index];
- const domain = try percentEncodedDomain(cmd.arena, params.url, params.domain);
+ const domain = try CdpStorage.percentEncodedDomain(cmd.arena, params.url, params.domain);
// TBD does chrome take the path from the url as default? (unlike setCookies)
if (cookieMatches(cookie, params.name, domain, params.path)) {
cookies.swapRemove(index).deinit();
@@ -134,130 +129,30 @@ fn clearBrowserCookies(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
-const SameSite = enum {
- Strict,
- Lax,
- None,
-};
-const CookiePriority = enum {
- Low,
- Medium,
- High,
-};
-const CookieSourceScheme = enum {
- Unset,
- NonSecure,
- Secure,
-};
-
-fn isHostChar(c: u8) bool {
- return switch (c) {
- 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
- '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
- ':' => true,
- '[', ']' => true,
- else => false,
- };
-}
-
-const CdpCookie = struct {
- name: []const u8,
- value: []const u8,
- url: ?[]const u8 = null,
- domain: ?[]const u8 = null,
- path: ?[]const u8 = null,
- secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
- httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
- sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
- expires: ?i64 = null, // -1? says google
- priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
- sameParty: ?bool = null,
- sourceScheme: ?CookieSourceScheme = null,
- // sourcePort: Temporary ability and it will be removed from CDP
- partitionKey: ?CookiePartitionKey = null,
-};
-
fn setCookie(cmd: anytype) !void {
const params = (try cmd.params(
- CdpCookie,
+ CdpStorage.CdpCookie,
)) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
- try setCdpCookie(&bc.session.cookie_jar, params);
+ try CdpStorage.setCdpCookie(&bc.session.cookie_jar, params);
try cmd.sendResult(.{ .success = true }, .{});
}
fn setCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
- cookies: []const CdpCookie,
+ cookies: []const CdpStorage.CdpCookie,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
for (params.cookies) |param| {
- try setCdpCookie(&bc.session.cookie_jar, param);
+ try CdpStorage.setCdpCookie(&bc.session.cookie_jar, param);
}
try cmd.sendResult(null, .{});
}
-fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
- if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
- return error.NotYetImplementedParams;
- }
- if (param.name.len == 0) return error.InvalidParams;
- if (param.value.len == 0) return error.InvalidParams;
-
- var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
- errdefer arena.deinit();
- const a = arena.allocator();
-
- // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
- const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
-
- const cookie = Cookie{
- .arena = arena,
- .name = try a.dupe(u8, param.name),
- .value = try a.dupe(u8, param.value),
- .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
- .domain = domain,
- .expires = param.expires,
- .secure = param.secure,
- .http_only = param.httpOnly,
- .same_site = switch (param.sameSite) {
- .Strict => .strict,
- .Lax => .lax,
- .None => .none,
- },
- };
- try cookie_jar.add(cookie, std.time.timestamp());
-}
-
-// Note: Chrome does not apply rules like removing a leading `.` from the domain.
-fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
- const toLower = @import("../../browser/storage/cookie.zig").toLower;
- if (domain) |domain_| {
- const output = try allocator.dupe(u8, domain_);
- return toLower(output);
- } else if (default_url) |url| {
- const uri = std.Uri.parse(url) catch return error.InvalidParams;
-
- var output: []u8 = undefined;
- switch (uri.host orelse return error.InvalidParams) {
- .raw => |str| {
- var list = std.ArrayList(u8).init(allocator);
- try list.ensureTotalCapacity(str.len); // Expect no precents needed
- try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
- output = list.items; // @memory retains memory used before growing
- },
- .percent_encoded => |str| {
- output = try allocator.dupe(u8, str);
- },
- }
- return toLower(output);
- } else return null;
-}
-
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
new file mode 100644
index 000000000..654d976be
--- /dev/null
+++ b/src/cdp/domains/storage.zig
@@ -0,0 +1,255 @@
+// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+
+const log = @import("../../log.zig");
+const Cookie = @import("../../browser/storage/storage.zig").Cookie;
+const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
+
+pub fn processMessage(cmd: anytype) !void {
+ const action = std.meta.stringToEnum(enum {
+ clearCookies,
+ setCookies,
+ getCookies,
+ }, cmd.input.action) orelse return error.UnknownMethod;
+
+ switch (action) {
+ .clearCookies => return clearCookies(cmd),
+ .getCookies => return getCookies(cmd),
+ .setCookies => return setCookies(cmd),
+ }
+}
+
+fn clearCookies(cmd: anytype) !void {
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ const params = (try cmd.params(struct {
+ browserContextId: ?[]const u8,
+ })) orelse return error.InvalidParams;
+
+ if (params.browserContextId) |browser_context_id| {
+ if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
+ return error.UnknownBrowserContextId;
+ }
+ }
+
+ bc.session.cookie_jar.clearRetainingCapacity();
+
+ return cmd.sendResult(null, .{});
+}
+
+fn getCookies(cmd: anytype) !void {
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ const params = (try cmd.params(struct {
+ browserContextId: ?[]const u8,
+ })) orelse return error.InvalidParams;
+
+ if (params.browserContextId) |browser_context_id| {
+ if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
+ return error.UnknownBrowserContextId;
+ }
+ }
+ const cookies = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
+ try cmd.sendResult(.{ .cookies = cookies }, .{});
+}
+
+fn setCookies(cmd: anytype) !void {
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ const params = (try cmd.params(struct {
+ cookies: []const CdpCookie,
+ browserContextId: ?[]const u8,
+ })) orelse return error.InvalidParams;
+
+ if (params.browserContextId) |browser_context_id| {
+ if (std.mem.eql(u8, browser_context_id, bc.id) == false) {
+ return error.UnknownBrowserContextId;
+ }
+ }
+
+ for (params.cookies) |param| {
+ try setCdpCookie(&bc.session.cookie_jar, param);
+ }
+
+ try cmd.sendResult(null, .{});
+}
+
+pub const SameSite = enum {
+ Strict,
+ Lax,
+ None,
+};
+pub const CookiePriority = enum {
+ Low,
+ Medium,
+ High,
+};
+pub const CookieSourceScheme = enum {
+ Unset,
+ NonSecure,
+ Secure,
+};
+
+pub const CookiePartitionKey = struct {
+ topLevelSite: []const u8,
+ hasCrossSiteAncestor: bool,
+};
+
+pub const CdpCookie = struct {
+ name: []const u8,
+ value: []const u8,
+ url: ?[]const u8 = null,
+ domain: ?[]const u8 = null,
+ path: ?[]const u8 = null,
+ secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
+ expires: ?i64 = null, // -1? says google
+ priority: CookiePriority = .Medium, // default: https://datatracker.ietf.org/doc/html/draft-west-cookie-priority-00
+ sameParty: ?bool = null,
+ sourceScheme: ?CookieSourceScheme = null,
+ // sourcePort: Temporary ability and it will be removed from CDP
+ partitionKey: ?CookiePartitionKey = null,
+};
+
+pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
+ if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
+ return error.NotYetImplementedParams;
+ }
+ if (param.name.len == 0) return error.InvalidParams;
+ if (param.value.len == 0) return error.InvalidParams;
+
+ var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
+ errdefer arena.deinit();
+ const a = arena.allocator();
+
+ // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
+ const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
+
+ const cookie = Cookie{
+ .arena = arena,
+ .name = try a.dupe(u8, param.name),
+ .value = try a.dupe(u8, param.value),
+ .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
+ .domain = domain,
+ .expires = param.expires,
+ .secure = param.secure,
+ .http_only = param.httpOnly,
+ .same_site = switch (param.sameSite) {
+ .Strict => .strict,
+ .Lax => .lax,
+ .None => .none,
+ },
+ };
+ try cookie_jar.add(cookie, std.time.timestamp());
+}
+
+// Note: Chrome does not apply rules like removing a leading `.` from the domain.
+pub fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
+ const toLower = @import("../../browser/storage/cookie.zig").toLower;
+ if (domain) |domain_| {
+ const output = try allocator.dupe(u8, domain_);
+ return toLower(output);
+ } else if (default_url) |url| {
+ const uri = std.Uri.parse(url) catch return error.InvalidParams;
+
+ var output: []u8 = undefined;
+ switch (uri.host orelse return error.InvalidParams) {
+ .raw => |str| {
+ var list = std.ArrayList(u8).init(allocator);
+ try list.ensureTotalCapacity(str.len); // Expect no precents needed
+ try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
+ output = list.items; // @memory retains memory used before growing
+ },
+ .percent_encoded => |str| {
+ output = try allocator.dupe(u8, str);
+ },
+ }
+ return toLower(output);
+ } else return null;
+}
+
+fn isHostChar(c: u8) bool {
+ return switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
+ '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
+ ':' => true,
+ '[', ']' => true,
+ else => false,
+ };
+}
+
+pub const CookieWriter = struct {
+ cookies: []const Cookie,
+
+ pub fn jsonStringify(self: *const CookieWriter, w: anytype) !void {
+ self.writeCookies(w) catch |err| {
+ // The only error our jsonStringify method can return is @TypeOf(w).Error.
+ log.err(.cdp, "json stringify", .{ .err = err });
+ return error.OutOfMemory;
+ };
+ }
+
+ fn writeCookies(self: CookieWriter, w: anytype) !void {
+ try w.beginArray();
+ for (self.cookies) |*cookie| {
+ try writeCookie(cookie, w);
+ }
+ try w.endArray();
+ }
+
+ fn writeCookie(cookie: *const Cookie, w: anytype) !void {
+ try w.beginObject();
+ {
+ try w.objectField("name");
+ try w.write(cookie.name);
+
+ try w.objectField("value");
+ try w.write(cookie.value);
+
+ try w.objectField("domain");
+ try w.write(cookie.domain);
+
+ try w.objectField("path");
+ try w.write(cookie.path);
+
+ try w.objectField("expires");
+ try w.write(cookie.expires orelse -1);
+
+ // TODO size
+
+ try w.objectField("httpOnly");
+ try w.write(cookie.http_only);
+
+ try w.objectField("secure");
+ try w.write(cookie.secure);
+
+ // TODO session
+
+ try w.objectField("sameSite");
+ switch (cookie.same_site) {
+ .none => try w.write("None"),
+ .lax => try w.write("Lax"),
+ .strict => try w.write("Strict"),
+ }
+
+ // TODO experimentals
+ }
+ try w.endObject();
+ }
+};
From c0c1b265aa2c3d97c48fcc7b02c637acebdfce45 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Thu, 12 Jun 2025 13:57:06 +0200
Subject: [PATCH 07/14] Network.getCookies
---
src/browser/storage/cookie.zig | 235 ++++++++++++---------------------
src/cdp/domains/network.zig | 41 +++++-
src/cdp/domains/storage.zig | 125 +++++++++++-------
3 files changed, 201 insertions(+), 200 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index aa93ba21b..db7a29a22 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -66,87 +66,33 @@ pub const Jar = struct {
}
}
- pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
- const target_path = target_uri.path.percent_encoded;
- const target_host = (target_uri.host orelse return error.InvalidURI).percent_encoded;
-
- const same_site = try areSameSite(opts.origin_uri, target_host);
- const is_secure = std.mem.eql(u8, target_uri.scheme, "https");
-
- var i: usize = 0;
- var cookies = self.cookies.items;
- const navigation = opts.navigation;
- const request_time = opts.request_time orelse std.time.timestamp();
-
- var first = true;
- while (i < cookies.len) {
- const cookie = &cookies[i];
-
- if (isCookieExpired(cookie, request_time)) {
- cookie.deinit();
- _ = self.cookies.swapRemove(i);
- // don't increment i !
- continue;
+ pub fn removeExpired(self: *Jar, request_time: ?i64) void {
+ if (self.cookies.items.len == 0) return;
+ const time = request_time orelse std.time.timestamp();
+ var i: usize = self.cookies.items.len - 1;
+ while (i > 0) {
+ defer i -= 1;
+ const cookie = &self.cookies.items[i];
+ if (isCookieExpired(cookie, time)) {
+ self.cookies.swapRemove(i).deinit();
}
- i += 1;
+ }
+ }
- if (is_secure == false and cookie.secure) {
- // secure cookie can only be sent over HTTPs
- continue;
- }
+ pub fn forRequest(self: *Jar, target_uri: *const Uri, writer: anytype, opts: LookupOpts) !void {
+ const target = PreparedUri{
+ .host = (target_uri.host orelse return error.InvalidURI).percent_encoded,
+ .path = target_uri.path.percent_encoded,
+ .secure = std.mem.eql(u8, target_uri.scheme, "https"),
+ };
+ const same_site = try areSameSite(opts.origin_uri, target.host);
- if (same_site == false) {
- // If we aren't on the "same site" (matching 2nd level domain
- // taking into account public suffix list), then the cookie
- // can only be sent if cookie.same_site == .none, or if
- // we're navigating to (as opposed to, say, loading an image)
- // and cookie.same_site == .lax
- switch (cookie.same_site) {
- .strict => continue,
- .lax => if (navigation == false) continue,
- .none => {},
- }
- }
+ removeExpired(self, opts.request_time);
- {
- const domain = cookie.domain;
- if (domain[0] == '.') {
- // When a Set-Cookie header has a Domain attribute
- // Then we will _always_ prefix it with a dot, extending its
- // availability to all subdomains (yes, setting the Domain
- // attributes EXPANDS the domains which the cookie will be
- // sent to, to always include all subdomains).
- if (std.mem.eql(u8, target_host, domain[1..]) == false and std.mem.endsWith(u8, target_host, domain) == false) {
- continue;
- }
- } else if (std.mem.eql(u8, target_host, domain) == false) {
- // When the Domain attribute isn't specific, then the cookie
- // is only sent on an exact match.
- continue;
- }
- }
+ var first = true;
+ for (self.cookies.items) |*cookie| {
+ if (!cookie.appliesTo(&target, same_site, opts.navigation)) continue;
- {
- const path = cookie.path;
- if (path[path.len - 1] == '/') {
- // If our cookie has a trailing slash, we can only match is
- // the target path is a perfix. I.e., if our path is
- // /doc/ we can only match /doc/*
- if (std.mem.startsWith(u8, target_path, path) == false) {
- continue;
- }
- } else {
- // Our cookie path is something like /hello
- if (std.mem.startsWith(u8, target_path, path) == false) {
- // The target path has to either be /hello (it isn't)
- continue;
- } else if (target_path.len < path.len or (target_path.len > path.len and target_path[path.len] != '/')) {
- // Or it has to be something like /hello/* (it isn't)
- // it isn't!
- continue;
- }
- }
- }
// we have a match!
if (first) {
first = false;
@@ -180,44 +126,6 @@ pub const Jar = struct {
}
};
-// pub const CookieList = struct {
-// _cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
-
-// pub fn deinit(self: *CookieList, allocator: Allocator) void {
-// self._cookies.deinit(allocator);
-// }
-
-// pub fn cookies(self: *const CookieList) []*const Cookie {
-// return self._cookies.items;
-// }
-
-// pub fn len(self: *const CookieList) usize {
-// return self._cookies.items.len;
-// }
-
-// pub fn write(self: *const CookieList, writer: anytype) !void {
-// const all = self._cookies.items;
-// if (all.len == 0) {
-// return;
-// }
-// try writeCookie(all[0], writer);
-// for (all[1..]) |cookie| {
-// try writer.writeAll("; ");
-// try writeCookie(cookie, writer);
-// }
-// }
-
-// fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
-// if (cookie.name.len > 0) {
-// try writer.writeAll(cookie.name);
-// try writer.writeByte('=');
-// }
-// if (cookie.value.len > 0) {
-// try writer.writeAll(cookie.value);
-// }
-// }
-// };
-
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
const ce = cookie.expires orelse return false;
return ce <= now;
@@ -447,6 +355,71 @@ pub const Cookie = struct {
const value = trim(str[sep + 1 .. key_value_end]);
return .{ name, value, rest };
}
+
+ pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool) bool {
+ if (url.secure == false and self.secure) {
+ // secure cookie can only be sent over HTTPs
+ return false;
+ }
+
+ if (same_site == false) {
+ // If we aren't on the "same site" (matching 2nd level domain
+ // taking into account public suffix list), then the cookie
+ // can only be sent if cookie.same_site == .none, or if
+ // we're navigating to (as opposed to, say, loading an image)
+ // and cookie.same_site == .lax
+ switch (self.same_site) {
+ .strict => return false,
+ .lax => if (navigation == false) return false,
+ .none => {},
+ }
+ }
+
+ {
+ if (self.domain[0] == '.') {
+ // When a Set-Cookie header has a Domain attribute
+ // Then we will _always_ prefix it with a dot, extending its
+ // availability to all subdomains (yes, setting the Domain
+ // attributes EXPANDS the domains which the cookie will be
+ // sent to, to always include all subdomains).
+ if (std.mem.eql(u8, url.host, self.domain[1..]) == false and std.mem.endsWith(u8, url.host, self.domain) == false) {
+ return false;
+ }
+ } else if (std.mem.eql(u8, url.host, self.domain) == false) {
+ // When the Domain attribute isn't specific, then the cookie
+ // is only sent on an exact match.
+ return false;
+ }
+ }
+
+ {
+ if (self.path[self.path.len - 1] == '/') {
+ // If our cookie has a trailing slash, we can only match is
+ // the target path is a perfix. I.e., if our path is
+ // /doc/ we can only match /doc/*
+ if (std.mem.startsWith(u8, url.path, self.path) == false) {
+ return false;
+ }
+ } else {
+ // Our cookie path is something like /hello
+ if (std.mem.startsWith(u8, url.path, self.path) == false) {
+ // The target path has to either be /hello (it isn't)
+ return false;
+ } else if (url.path.len < self.path.len or (url.path.len > self.path.len and url.path[self.path.len] != '/')) {
+ // Or it has to be something like /hello/* (it isn't)
+ // it isn't!
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+};
+
+pub const PreparedUri = struct {
+ host: []const u8, // Percent encoded, lower case
+ path: []const u8, // Percent encoded
+ secure: bool, // True if scheme is https
};
fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 {
@@ -675,40 +648,6 @@ test "Jar: forRequest" {
// the 'global2' cookie
}
-// test "CookieList: write" {
-// var arr: std.ArrayListUnmanaged(u8) = .{};
-// defer arr.deinit(testing.allocator);
-
-// var cookie_list = CookieList{};
-// defer cookie_list.deinit(testing.allocator);
-
-// const c1 = try Cookie.parse(testing.allocator, &test_uri, "cookie_name=cookie_value");
-// defer c1.deinit();
-// {
-// try cookie_list._cookies.append(testing.allocator, &c1);
-// try cookie_list.write(arr.writer(testing.allocator));
-// try testing.expectEqual("cookie_name=cookie_value", arr.items);
-// }
-
-// const c2 = try Cookie.parse(testing.allocator, &test_uri, "x84");
-// defer c2.deinit();
-// {
-// arr.clearRetainingCapacity();
-// try cookie_list._cookies.append(testing.allocator, &c2);
-// try cookie_list.write(arr.writer(testing.allocator));
-// try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
-// }
-
-// const c3 = try Cookie.parse(testing.allocator, &test_uri, "nope=");
-// defer c3.deinit();
-// {
-// arr.clearRetainingCapacity();
-// try cookie_list._cookies.append(testing.allocator, &c3);
-// try cookie_list.write(arr.writer(testing.allocator));
-// try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
-// }
-// }
-
test "Cookie: parse key=value" {
try expectError(error.Empty, null, "");
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index c7ad0c609..9c0fe27b0 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -33,6 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
clearBrowserCookies,
setCookie,
setCookies,
+ getCookies,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
@@ -44,6 +45,7 @@ pub fn processMessage(cmd: anytype) !void {
.clearBrowserCookies => return clearBrowserCookies(cmd),
.setCookie => return setCookie(cmd),
.setCookies => return setCookies(cmd),
+ .getCookies => return getCookies(cmd),
}
}
@@ -82,6 +84,7 @@ fn setExtraHTTPHeaders(cmd: anytype) !void {
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
+// Only matches the cookie on provided parameters
fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, path: ?[]const u8) bool {
if (!std.mem.eql(u8, cookie.name, name)) return false;
@@ -91,10 +94,15 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p
if (path) |path_| {
if (!std.mem.eql(u8, cookie.path, path_)) return false;
}
-
return true;
}
+// Only matches the cookie on provided parameters
+fn cookieAppliesTo(cookie: *const Cookie, domain: []const u8, path: []const u8) bool {
+ if (!std.mem.eql(u8, cookie.domain, domain)) return false;
+ return std.mem.startsWith(u8, path, cookie.path);
+}
+
fn deleteCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
name: []const u8,
@@ -107,11 +115,13 @@ fn deleteCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const cookies = &bc.session.cookie_jar.cookies;
+ const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
+
var index = cookies.items.len;
while (index > 0) {
index -= 1;
const cookie = &cookies.items[index];
- const domain = try CdpStorage.percentEncodedDomain(cmd.arena, params.url, params.domain);
+ const domain = try CdpStorage.percentEncodedDomainOrHost(cmd.arena, uri, params.domain);
// TBD does chrome take the path from the url as default? (unlike setCookies)
if (cookieMatches(cookie, params.name, domain, params.path)) {
cookies.swapRemove(index).deinit();
@@ -153,6 +163,33 @@ fn setCookies(cmd: anytype) !void {
try cmd.sendResult(null, .{});
}
+fn getCookies(cmd: anytype) !void {
+ const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
+ const params = (try cmd.params(struct {
+ urls: []const []const u8,
+ })) orelse return error.InvalidParams;
+
+ var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, params.urls.len);
+ for (params.urls) |url| {
+ const uri = std.Uri.parse(url) catch return error.InvalidParams;
+
+ const host_component = uri.host orelse return error.InvalidParams;
+ const host = CdpStorage.toLower(try CdpStorage.percentEncode(cmd.arena, host_component, CdpStorage.isHostChar));
+
+ var path: []const u8 = try CdpStorage.percentEncode(cmd.arena, uri.path, CdpStorage.isPathChar);
+ if (path.len == 0) path = "/";
+
+ const secure = std.mem.eql(u8, uri.scheme, "https");
+
+ urls.appendAssumeCapacity(.{ .host = host, .path = path, .secure = secure });
+ }
+
+ var jar = &bc.session.cookie_jar;
+ jar.removeExpired(null);
+ const writer = CdpStorage.CookieWriter{ .cookies = jar.cookies.items, .urls = urls.items };
+ try cmd.sendResult(.{ .cookies = writer }, .{});
+}
+
// Upsert a header into the headers array.
// returns true if the header was added, false if it was updated
fn putAssumeCapacity(headers: *std.ArrayListUnmanaged(std.http.Header), extra: std.http.Header) bool {
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index 654d976be..2efd1c35b 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -22,6 +22,8 @@ const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
+pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
+pub const toLower = @import("../../browser/storage/cookie.zig").toLower;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -65,6 +67,7 @@ fn getCookies(cmd: anytype) !void {
return error.UnknownBrowserContextId;
}
}
+ bc.session.cookie_jar.removeExpired(null);
const cookies = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
try cmd.sendResult(.{ .cookies = cookies }, .{});
}
@@ -139,7 +142,8 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
const a = arena.allocator();
// NOTE: The param.url can affect the default domain, path, source port, and source scheme.
- const domain = try percentEncodedDomain(a, param.url, param.domain) orelse return error.InvalidParams;
+ const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
+ const domain = try percentEncodedDomainOrHost(a, uri, param.domain) orelse return error.InvalidParams;
const cookie = Cookie{
.arena = arena,
@@ -160,31 +164,32 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
}
// Note: Chrome does not apply rules like removing a leading `.` from the domain.
-pub fn percentEncodedDomain(allocator: Allocator, default_url: ?[]const u8, domain: ?[]const u8) !?[]const u8 {
- const toLower = @import("../../browser/storage/cookie.zig").toLower;
+pub fn percentEncodedDomainOrHost(allocator: Allocator, default_url: ?std.Uri, domain: ?[]const u8) !?[]const u8 {
if (domain) |domain_| {
const output = try allocator.dupe(u8, domain_);
return toLower(output);
} else if (default_url) |url| {
- const uri = std.Uri.parse(url) catch return error.InvalidParams;
-
- var output: []u8 = undefined;
- switch (uri.host orelse return error.InvalidParams) {
- .raw => |str| {
- var list = std.ArrayList(u8).init(allocator);
- try list.ensureTotalCapacity(str.len); // Expect no precents needed
- try std.Uri.Component.percentEncode(list.writer(), str, isHostChar);
- output = list.items; // @memory retains memory used before growing
- },
- .percent_encoded => |str| {
- output = try allocator.dupe(u8, str);
- },
- }
+ const host = url.host orelse return error.InvalidParams;
+ const output = try percentEncode(allocator, host, isHostChar); // TODO remove subdomains
return toLower(output);
} else return null;
}
-fn isHostChar(c: u8) bool {
+pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
+ switch (component) {
+ .raw => |str| {
+ var list = std.ArrayList(u8).init(arena);
+ try list.ensureTotalCapacity(str.len); // Expect no precents needed
+ try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
+ return list.items; // @memory retains memory used before growing
+ },
+ .percent_encoded => |str| {
+ return try arena.dupe(u8, str);
+ },
+ }
+}
+
+pub fn isHostChar(c: u8) bool {
return switch (c) {
'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
'!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
@@ -194,8 +199,18 @@ fn isHostChar(c: u8) bool {
};
}
+pub fn isPathChar(c: u8) bool {
+ return switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
+ '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
+ '/', ':', '@' => true,
+ else => false,
+ };
+}
+
pub const CookieWriter = struct {
cookies: []const Cookie,
+ urls: ?[]const PreparedUri = null,
pub fn jsonStringify(self: *const CookieWriter, w: anytype) !void {
self.writeCookies(w) catch |err| {
@@ -207,49 +222,59 @@ pub const CookieWriter = struct {
fn writeCookies(self: CookieWriter, w: anytype) !void {
try w.beginArray();
- for (self.cookies) |*cookie| {
- try writeCookie(cookie, w);
+ if (self.urls) |urls| {
+ for (self.cookies) |*cookie| {
+ for (urls) |*url| {
+ if (cookie.appliesTo(url, false, false)) { // TBD same_site, should we compare to the pages url?
+ try writeCookie(cookie, w);
+ break;
+ }
+ }
+ }
+ } else {
+ for (self.cookies) |*cookie| {
+ try writeCookie(cookie, w);
+ }
}
try w.endArray();
}
+};
+pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
+ try w.beginObject();
+ {
+ try w.objectField("name");
+ try w.write(cookie.name);
- fn writeCookie(cookie: *const Cookie, w: anytype) !void {
- try w.beginObject();
- {
- try w.objectField("name");
- try w.write(cookie.name);
-
- try w.objectField("value");
- try w.write(cookie.value);
-
- try w.objectField("domain");
- try w.write(cookie.domain);
+ try w.objectField("value");
+ try w.write(cookie.value);
- try w.objectField("path");
- try w.write(cookie.path);
+ try w.objectField("domain");
+ try w.write(cookie.domain);
- try w.objectField("expires");
- try w.write(cookie.expires orelse -1);
+ try w.objectField("path");
+ try w.write(cookie.path);
- // TODO size
+ try w.objectField("expires");
+ try w.write(cookie.expires orelse -1);
- try w.objectField("httpOnly");
- try w.write(cookie.http_only);
+ // TODO size
- try w.objectField("secure");
- try w.write(cookie.secure);
+ try w.objectField("httpOnly");
+ try w.write(cookie.http_only);
- // TODO session
+ try w.objectField("secure");
+ try w.write(cookie.secure);
- try w.objectField("sameSite");
- switch (cookie.same_site) {
- .none => try w.write("None"),
- .lax => try w.write("Lax"),
- .strict => try w.write("Strict"),
- }
+ // TODO session
- // TODO experimentals
+ try w.objectField("sameSite");
+ switch (cookie.same_site) {
+ .none => try w.write("None"),
+ .lax => try w.write("Lax"),
+ .strict => try w.write("Strict"),
}
- try w.endObject();
+
+ // TODO experimentals
}
-};
+ try w.endObject();
+}
From 4213a7cd2cc108481e5b0790a51f20715fa4eaa2 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Fri, 13 Jun 2025 09:45:57 +0200
Subject: [PATCH 08/14] wip
---
src/browser/storage/cookie.zig | 2 +-
src/cdp/domains/storage.zig | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index db7a29a22..0c7949d50 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -312,7 +312,7 @@ pub const Cookie = struct {
@memcpy(s[1..], d);
break :blk s;
} else blk: {
- break :blk try aa.dupe(u8, host);
+ break :blk try aa.dupe(u8, host); // Sjors: Should subdomains be removed from host?
};
_ = toLower(owned_domain);
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index 2efd1c35b..78deed9d8 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -1,4 +1,4 @@
-// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier
// Pierre Tachoire
@@ -68,8 +68,8 @@ fn getCookies(cmd: anytype) !void {
}
}
bc.session.cookie_jar.removeExpired(null);
- const cookies = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
- try cmd.sendResult(.{ .cookies = cookies }, .{});
+ const writer = CookieWriter{ .cookies = bc.session.cookie_jar.cookies.items };
+ try cmd.sendResult(.{ .cookies = writer }, .{});
}
fn setCookies(cmd: anytype) !void {
@@ -143,7 +143,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
// NOTE: The param.url can affect the default domain, path, source port, and source scheme.
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
- const domain = try percentEncodedDomainOrHost(a, uri, param.domain) orelse return error.InvalidParams;
+ const domain = try percentEncodedDomainOrHost(a, uri, param.domain) orelse return error.InvalidParams; // TODO Domain needs to be prefixed with a dot if is explicitely set
const cookie = Cookie{
.arena = arena,
From 147573d8b327d506c7d376a1e1fd46df9d34915e Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Fri, 13 Jun 2025 12:37:19 +0200
Subject: [PATCH 09/14] refactor path / domain parsing
---
src/browser/storage/cookie.zig | 146 +++++++++++++++++++++------------
src/cdp/domains/storage.zig | 2 +-
src/runtime/js.zig | 2 +-
3 files changed, 94 insertions(+), 56 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 0c7949d50..ca96531dc 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -207,9 +207,6 @@ pub const Cookie = struct {
// this check is necessary, `std.mem.minMax` asserts len > 0
return error.Empty;
}
-
- const host = (uri.host orelse return error.InvalidURI).percent_encoded;
-
{
const min, const max = std.mem.minMax(u8, str);
if (min < 32 or max > 126) {
@@ -254,34 +251,10 @@ pub const Cookie = struct {
samesite,
}, std.ascii.lowerString(&scrap, key_string)) orelse continue;
- var value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
+ const value = if (sep == attribute.len) "" else trim(attribute[sep + 1 ..]);
switch (key) {
- .path => {
- // path attribute value either begins with a '/' or we
- // ignore it and use the "default-path" algorithm
- if (value.len > 0 and value[0] == '/') {
- path = value;
- }
- },
- .domain => {
- if (value.len == 0) {
- continue;
- }
- if (value[0] == '.') {
- // leading dot is ignored
- value = value[1..];
- }
-
- if (std.mem.indexOfScalarPos(u8, value, 0, '.') == null and std.ascii.eqlIgnoreCase("localhost", value) == false) {
- // can't set a cookie for a TLD
- return error.InvalidDomain;
- }
-
- if (std.mem.endsWith(u8, host, value) == false) {
- return error.InvalidDomain;
- }
- domain = value; // Domain is made lower case after it has relocated to the arena
- },
+ .path => path = value,
+ .domain => domain = value,
.secure => secure = true,
.@"max-age" => max_age = std.fmt.parseInt(i64, value, 10) catch continue,
.expires => expires = DateTime.parse(value, .rfc822) catch continue,
@@ -301,20 +274,9 @@ pub const Cookie = struct {
const aa = arena.allocator();
const owned_name = try aa.dupe(u8, cookie_name);
const owned_value = try aa.dupe(u8, cookie_value);
- const owned_path = if (path) |p|
- try aa.dupe(u8, p)
- else
- try defaultPath(aa, uri.path.percent_encoded);
-
- const owned_domain = if (domain) |d| blk: {
- const s = try aa.alloc(u8, d.len + 1);
- s[0] = '.';
- @memcpy(s[1..], d);
- break :blk s;
- } else blk: {
- break :blk try aa.dupe(u8, host); // Sjors: Should subdomains be removed from host?
- };
- _ = toLower(owned_domain);
+ const owned_path = try parse_path(aa, uri.path, path);
+ const host = uri.host orelse return error.InvalidURI;
+ const owned_domain = try parse_domain(aa, host, domain);
var normalized_expires: ?i64 = null;
if (max_age) |ma| {
@@ -339,6 +301,92 @@ pub const Cookie = struct {
};
}
+ pub fn parse_path(arena: Allocator, url_path: std.Uri.Component, explicit_path: ?[]const u8) ![]const u8 {
+ // path attribute value either begins with a '/' or we
+ // ignore it and use the "default-path" algorithm
+ if (explicit_path) |path| {
+ if (path.len > 0 and path[0] == '/') {
+ return try arena.dupe(u8, path);
+ }
+ }
+
+ // default-path
+ const either = url_path.percent_encoded;
+ if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
+ return "/";
+ }
+
+ var owned_path: []const u8 = try percentEncode(arena, url_path, isPathChar);
+ const last = std.mem.lastIndexOfScalar(u8, owned_path[1..], '/') orelse {
+ return "/";
+ };
+ return try arena.dupe(u8, owned_path[0 .. last + 1]);
+ }
+
+ pub fn parse_domain(arena: Allocator, url_host: std.Uri.Component, explicit_domain: ?[]const u8) ![]const u8 {
+ const encoded_host = try percentEncode(arena, url_host, isHostChar);
+ _ = toLower(encoded_host);
+
+ if (explicit_domain) |domain| {
+ if (domain.len > 0) {
+ const no_leading_dot = if (domain[0] == '.') domain[1..] else domain;
+
+ var list = std.ArrayList(u8).init(arena);
+ try list.ensureTotalCapacity(no_leading_dot.len + 1); // Expect no precents needed
+ list.appendAssumeCapacity('.');
+ try std.Uri.Component.percentEncode(list.writer(), no_leading_dot, isHostChar);
+ var owned_domain: []u8 = list.items; // @memory retains memory used before growing
+ _ = toLower(owned_domain);
+
+ if (std.mem.indexOfScalarPos(u8, owned_domain, 1, '.') == null and std.mem.eql(u8, "localhost", owned_domain[1..]) == false) {
+ // can't set a cookie for a TLD
+ return error.InvalidDomain;
+ }
+ if (std.mem.endsWith(u8, encoded_host, owned_domain[1..]) == false) {
+ return error.InvalidDomain;
+ }
+ return owned_domain;
+ }
+ }
+
+ return encoded_host; // default-domain
+ }
+
+ // TODO when getting cookeis Note: Chrome does not apply rules like removing a leading `.` from the domain.
+
+ pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
+ switch (component) {
+ .raw => |str| {
+ var list = std.ArrayList(u8).init(arena);
+ try list.ensureTotalCapacity(str.len); // Expect no precents needed
+ try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
+ return list.items; // @memory retains memory used before growing
+ },
+ .percent_encoded => |str| {
+ return try arena.dupe(u8, str);
+ },
+ }
+ }
+
+ pub fn isHostChar(c: u8) bool {
+ return switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
+ '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
+ ':' => true,
+ '[', ']' => true,
+ else => false,
+ };
+ }
+
+ pub fn isPathChar(c: u8) bool {
+ return switch (c) {
+ 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
+ '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
+ '/', ':', '@' => true,
+ else => false,
+ };
+ }
+
fn parseNameValue(str: []const u8) !struct { []const u8, []const u8, []const u8 } {
const key_value_end = std.mem.indexOfScalarPos(u8, str, 0, ';') orelse str.len;
const rest = if (key_value_end == str.len) "" else str[key_value_end + 1 ..];
@@ -422,16 +470,6 @@ pub const PreparedUri = struct {
secure: bool, // True if scheme is https
};
-fn defaultPath(allocator: Allocator, document_path: []const u8) ![]const u8 {
- if (document_path.len == 0 or (document_path.len == 1 and document_path[0] == '/')) {
- return "/";
- }
- const last = std.mem.lastIndexOfScalar(u8, document_path[1..], '/') orelse {
- return "/";
- };
- return try allocator.dupe(u8, document_path[0 .. last + 1]);
-}
-
fn trim(str: []const u8) []const u8 {
return std.mem.trim(u8, str, &std.ascii.whitespace);
}
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index 78deed9d8..f4983c3f8 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -249,7 +249,7 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
try w.write(cookie.value);
try w.objectField("domain");
- try w.write(cookie.domain);
+ try w.write(cookie.domain); // Should we hide a leading dot?
try w.objectField("path");
try w.write(cookie.path);
diff --git a/src/runtime/js.zig b/src/runtime/js.zig
index 86619c6a4..a86db0b5a 100644
--- a/src/runtime/js.zig
+++ b/src/runtime/js.zig
@@ -400,7 +400,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
};
// For a Page we only create one HandleScope, it is stored in the main World (enter==true). A page can have multple contexts, 1 for each World.
- // The main Context/Scope that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
+ // The main Context that enters and holds the HandleScope should therefore always be created first. Following other worlds for this page
// like isolated Worlds, will thereby place their objects on the main page's HandleScope. Note: In the furure the number of context will multiply multiple frames support
var handle_scope: ?v8.HandleScope = null;
if (enter) {
From e0c5c4b4e9c134a11231559185814895c6afcda7 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Fri, 13 Jun 2025 13:35:38 +0200
Subject: [PATCH 10/14] Cleaning up crumbles
---
src/browser/storage/cookie.zig | 29 ++++++++++++-------
src/cdp/domains/network.zig | 24 ++++++++--------
src/cdp/domains/storage.zig | 52 +++-------------------------------
3 files changed, 35 insertions(+), 70 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index ca96531dc..90a239074 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -274,9 +274,8 @@ pub const Cookie = struct {
const aa = arena.allocator();
const owned_name = try aa.dupe(u8, cookie_name);
const owned_value = try aa.dupe(u8, cookie_value);
- const owned_path = try parse_path(aa, uri.path, path);
- const host = uri.host orelse return error.InvalidURI;
- const owned_domain = try parse_domain(aa, host, domain);
+ const owned_path = try parsePath(aa, uri, path);
+ const owned_domain = try parseDomain(aa, uri, domain);
var normalized_expires: ?i64 = null;
if (max_age) |ma| {
@@ -301,7 +300,7 @@ pub const Cookie = struct {
};
}
- pub fn parse_path(arena: Allocator, url_path: std.Uri.Component, explicit_path: ?[]const u8) ![]const u8 {
+ pub fn parsePath(arena: Allocator, uri: ?*const std.Uri, explicit_path: ?[]const u8) ![]const u8 {
// path attribute value either begins with a '/' or we
// ignore it and use the "default-path" algorithm
if (explicit_path) |path| {
@@ -311,6 +310,8 @@ pub const Cookie = struct {
}
// default-path
+ const url_path = (uri orelse return "/").path;
+
const either = url_path.percent_encoded;
if (either.len == 0 or (either.len == 1 and either[0] == '/')) {
return "/";
@@ -323,9 +324,14 @@ pub const Cookie = struct {
return try arena.dupe(u8, owned_path[0 .. last + 1]);
}
- pub fn parse_domain(arena: Allocator, url_host: std.Uri.Component, explicit_domain: ?[]const u8) ![]const u8 {
- const encoded_host = try percentEncode(arena, url_host, isHostChar);
- _ = toLower(encoded_host);
+ pub fn parseDomain(arena: Allocator, uri: ?*const std.Uri, explicit_domain: ?[]const u8) ![]const u8 {
+ var encoded_host: ?[]const u8 = null;
+ if (uri) |uri_| {
+ const uri_host = uri_.host orelse return error.InvalidURI;
+ const host = try percentEncode(arena, uri_host, isHostChar);
+ _ = toLower(host);
+ encoded_host = host;
+ }
if (explicit_domain) |domain| {
if (domain.len > 0) {
@@ -342,14 +348,17 @@ pub const Cookie = struct {
// can't set a cookie for a TLD
return error.InvalidDomain;
}
- if (std.mem.endsWith(u8, encoded_host, owned_domain[1..]) == false) {
- return error.InvalidDomain;
+ if (encoded_host) |host| {
+ if (std.mem.endsWith(u8, host, owned_domain[1..]) == false) {
+ return error.InvalidDomain;
+ }
}
+
return owned_domain;
}
}
- return encoded_host; // default-domain
+ return encoded_host orelse return error.InvalidDomain; // default-domain
}
// TODO when getting cookeis Note: Chrome does not apply rules like removing a leading `.` from the domain.
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 9c0fe27b0..e0678186b 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -116,14 +116,18 @@ fn deleteCookies(cmd: anytype) !void {
const cookies = &bc.session.cookie_jar.cookies;
const uri = if (params.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
+ const uri_ptr = if (uri) |u| &u else null;
var index = cookies.items.len;
while (index > 0) {
index -= 1;
const cookie = &cookies.items[index];
- const domain = try CdpStorage.percentEncodedDomainOrHost(cmd.arena, uri, params.domain);
- // TBD does chrome take the path from the url as default? (unlike setCookies)
- if (cookieMatches(cookie, params.name, domain, params.path)) {
+ const domain = try Cookie.parseDomain(cmd.arena, uri_ptr, params.domain);
+ const path = try Cookie.parsePath(cmd.arena, uri_ptr, params.path);
+
+ // We do not want to use Cookie.appliesTo here. As a Cookie with a shorter path would match.
+ // Similar to deduplicating with areCookiesEqual, except domain and path are optional.
+ if (cookieMatches(cookie, params.name, domain, path)) {
cookies.swapRemove(index).deinit();
}
}
@@ -173,15 +177,11 @@ fn getCookies(cmd: anytype) !void {
for (params.urls) |url| {
const uri = std.Uri.parse(url) catch return error.InvalidParams;
- const host_component = uri.host orelse return error.InvalidParams;
- const host = CdpStorage.toLower(try CdpStorage.percentEncode(cmd.arena, host_component, CdpStorage.isHostChar));
-
- var path: []const u8 = try CdpStorage.percentEncode(cmd.arena, uri.path, CdpStorage.isPathChar);
- if (path.len == 0) path = "/";
-
- const secure = std.mem.eql(u8, uri.scheme, "https");
-
- urls.appendAssumeCapacity(.{ .host = host, .path = path, .secure = secure });
+ urls.appendAssumeCapacity(.{
+ .host = try Cookie.parseDomain(cmd.arena, &uri, null),
+ .path = try Cookie.parsePath(cmd.arena, &uri, null),
+ .secure = std.mem.eql(u8, uri.scheme, "https"),
+ });
}
var jar = &bc.session.cookie_jar;
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index f4983c3f8..033c67b79 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -23,7 +23,6 @@ const log = @import("../../log.zig");
const Cookie = @import("../../browser/storage/storage.zig").Cookie;
const CookieJar = @import("../../browser/storage/storage.zig").CookieJar;
pub const PreparedUri = @import("../../browser/storage/cookie.zig").PreparedUri;
-pub const toLower = @import("../../browser/storage/cookie.zig").toLower;
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
@@ -143,13 +142,15 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
// NOTE: The param.url can affect the default domain, path, source port, and source scheme.
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
- const domain = try percentEncodedDomainOrHost(a, uri, param.domain) orelse return error.InvalidParams; // TODO Domain needs to be prefixed with a dot if is explicitely set
+ const uri_ptr = if (uri) |*u| u else null;
+ const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
+ const path = try Cookie.parsePath(a, uri_ptr, param.path);
const cookie = Cookie{
.arena = arena,
.name = try a.dupe(u8, param.name),
.value = try a.dupe(u8, param.value),
- .path = if (param.path) |path| try a.dupe(u8, path) else "/", // Chrome does not actually take the path from the url and just defaults to "/".
+ .path = path,
.domain = domain,
.expires = param.expires,
.secure = param.secure,
@@ -163,51 +164,6 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
try cookie_jar.add(cookie, std.time.timestamp());
}
-// Note: Chrome does not apply rules like removing a leading `.` from the domain.
-pub fn percentEncodedDomainOrHost(allocator: Allocator, default_url: ?std.Uri, domain: ?[]const u8) !?[]const u8 {
- if (domain) |domain_| {
- const output = try allocator.dupe(u8, domain_);
- return toLower(output);
- } else if (default_url) |url| {
- const host = url.host orelse return error.InvalidParams;
- const output = try percentEncode(allocator, host, isHostChar); // TODO remove subdomains
- return toLower(output);
- } else return null;
-}
-
-pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
- switch (component) {
- .raw => |str| {
- var list = std.ArrayList(u8).init(arena);
- try list.ensureTotalCapacity(str.len); // Expect no precents needed
- try std.Uri.Component.percentEncode(list.writer(), str, isValidChar);
- return list.items; // @memory retains memory used before growing
- },
- .percent_encoded => |str| {
- return try arena.dupe(u8, str);
- },
- }
-}
-
-pub fn isHostChar(c: u8) bool {
- return switch (c) {
- 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
- '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
- ':' => true,
- '[', ']' => true,
- else => false,
- };
-}
-
-pub fn isPathChar(c: u8) bool {
- return switch (c) {
- 'A'...'Z', 'a'...'z', '0'...'9', '-', '.', '_', '~' => true,
- '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' => true,
- '/', ':', '@' => true,
- else => false,
- };
-}
-
pub const CookieWriter = struct {
cookies: []const Cookie,
urls: ?[]const PreparedUri = null,
From a6ad4800bf1d9a639445410d70111bc614067027 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Fri, 13 Jun 2025 15:41:08 +0200
Subject: [PATCH 11/14] CDP.Storage cookies tests
---
src/browser/storage/cookie.zig | 2 -
src/cdp/domains/storage.zig | 84 ++++++++++++++++++++++++++++++----
2 files changed, 76 insertions(+), 10 deletions(-)
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 90a239074..1abd37e1c 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -361,8 +361,6 @@ pub const Cookie = struct {
return encoded_host orelse return error.InvalidDomain; // default-domain
}
- // TODO when getting cookeis Note: Chrome does not apply rules like removing a leading `.` from the domain.
-
pub fn percentEncode(arena: Allocator, component: std.Uri.Component, comptime isValidChar: fn (u8) bool) ![]u8 {
switch (component) {
.raw => |str| {
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index 033c67b79..cc26437b1 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -41,7 +41,7 @@ pub fn processMessage(cmd: anytype) !void {
fn clearCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
- browserContextId: ?[]const u8,
+ browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
if (params.browserContextId) |browser_context_id| {
@@ -58,7 +58,7 @@ fn clearCookies(cmd: anytype) !void {
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
- browserContextId: ?[]const u8,
+ browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
if (params.browserContextId) |browser_context_id| {
@@ -75,7 +75,7 @@ fn setCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
cookies: []const CdpCookie,
- browserContextId: ?[]const u8,
+ browserContextId: ?[]const u8 = null,
})) orelse return error.InvalidParams;
if (params.browserContextId) |browser_context_id| {
@@ -118,7 +118,7 @@ pub const CdpCookie = struct {
url: ?[]const u8 = null,
domain: ?[]const u8 = null,
path: ?[]const u8 = null,
- secure: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
+ secure: ?bool = null, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
httpOnly: bool = false, // default: https://www.rfc-editor.org/rfc/rfc6265#section-5.3
sameSite: SameSite = .None, // default: https://datatracker.ietf.org/doc/html/draft-west-first-party-cookies
expires: ?i64 = null, // -1? says google
@@ -140,11 +140,13 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
errdefer arena.deinit();
const a = arena.allocator();
- // NOTE: The param.url can affect the default domain, path, source port, and source scheme.
+ // NOTE: The param.url can affect the default domain, (NOT path), secure, source port, and source scheme.
const uri = if (param.url) |url| std.Uri.parse(url) catch return error.InvalidParams else null;
const uri_ptr = if (uri) |*u| u else null;
const domain = try Cookie.parseDomain(a, uri_ptr, param.domain);
- const path = try Cookie.parsePath(a, uri_ptr, param.path);
+ const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
+
+ const secure = if (param.secure) |s| s else if (uri) |uri_| std.mem.eql(u8, uri_.scheme, "https") else false;
const cookie = Cookie{
.arena = arena,
@@ -153,7 +155,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
.path = path,
.domain = domain,
.expires = param.expires,
- .secure = param.secure,
+ .secure = secure,
.http_only = param.httpOnly,
.same_site = switch (param.sameSite) {
.Strict => .strict,
@@ -181,7 +183,7 @@ pub const CookieWriter = struct {
if (self.urls) |urls| {
for (self.cookies) |*cookie| {
for (urls) |*url| {
- if (cookie.appliesTo(url, false, false)) { // TBD same_site, should we compare to the pages url?
+ if (cookie.appliesTo(url, true, true)) { // TBD same_site, should we compare to the pages url?
try writeCookie(cookie, w);
break;
}
@@ -234,3 +236,69 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
}
try w.endObject();
}
+
+const testing = @import("../testing.zig");
+
+test "cdp.Storage: cookies" {
+ var ctx = testing.context();
+ defer ctx.deinit();
+ _ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
+
+ // Initially empty
+ try ctx.processMessage(.{
+ .id = 3,
+ .method = "Storage.getCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
+
+ // Has cookies after setting them
+ try ctx.processMessage(.{
+ .id = 4,
+ .method = "Storage.setCookies",
+ .params = .{
+ .cookies = &[_]CdpCookie{
+ .{ .name = "test", .value = "value", .domain = "example.com", .path = "/mango" },
+ .{ .name = "test2", .value = "value2", .url = "https://car.example.com/pancakes" },
+ },
+ .browserContextId = "BID-S",
+ },
+ });
+ try ctx.expectSentResult(null, .{ .id = 4 });
+ try ctx.processMessage(.{
+ .id = 5,
+ .method = "Storage.getCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ try ctx.expectSentResult(.{
+ .cookies = &[_]ResCookie{
+ .{ .name = "test", .value = "value", .domain = ".example.com", .path = "/mango" },
+ .{ .name = "test2", .value = "value2", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
+ },
+ }, .{ .id = 5 });
+
+ // Empty after clearing cookies
+ try ctx.processMessage(.{
+ .id = 6,
+ .method = "Storage.clearCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ try ctx.expectSentResult(null, .{ .id = 6 });
+ try ctx.processMessage(.{
+ .id = 7,
+ .method = "Storage.getCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 7 });
+}
+
+pub const ResCookie = struct {
+ name: []const u8,
+ value: []const u8,
+ domain: []const u8,
+ path: []const u8 = "/",
+ expires: i32 = -1,
+ httpOnly: bool = false,
+ secure: bool = false,
+ sameSite: []const u8 = "None",
+};
From ba4c0338b03a2df15ac2aea9d8c0d1cd272b469e Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Fri, 13 Jun 2025 17:34:41 +0200
Subject: [PATCH 12/14] CDP Network cookie tests
---
src/cdp/domains/network.zig | 100 +++++++++++++++++++++++++++++++-----
1 file changed, 86 insertions(+), 14 deletions(-)
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index e0678186b..2cee435df 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -89,7 +89,9 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p
if (!std.mem.eql(u8, cookie.name, name)) return false;
if (domain) |domain_| {
- if (!std.mem.eql(u8, cookie.domain, domain_)) return false;
+ const c_no_dot = if (std.mem.startsWith(u8, cookie.domain, ".")) cookie.domain[1..] else cookie.domain;
+ const d_no_dot = if (std.mem.startsWith(u8, domain_, ".")) domain_[1..] else domain_;
+ if (!std.mem.eql(u8, c_no_dot, d_no_dot)) return false;
}
if (path) |path_| {
if (!std.mem.eql(u8, cookie.path, path_)) return false;
@@ -97,20 +99,15 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p
return true;
}
-// Only matches the cookie on provided parameters
-fn cookieAppliesTo(cookie: *const Cookie, domain: []const u8, path: []const u8) bool {
- if (!std.mem.eql(u8, cookie.domain, domain)) return false;
- return std.mem.startsWith(u8, path, cookie.path);
-}
-
fn deleteCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
name: []const u8,
url: ?[]const u8 = null,
domain: ?[]const u8 = null,
path: ?[]const u8 = null,
- // partitionKey: ?CookiePartitionKey,
+ partitionKey: ?CdpStorage.CookiePartitionKey = null,
})) orelse return error.InvalidParams;
+ if (params.partitionKey != null) return error.NotYetImplementedParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const cookies = &bc.session.cookie_jar.cookies;
@@ -135,11 +132,8 @@ fn deleteCookies(cmd: anytype) !void {
}
fn clearBrowserCookies(cmd: anytype) !void {
- _ = (try cmd.params(struct {})) orelse return error.InvalidParams;
-
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.session.cookie_jar.clearRetainingCapacity();
-
return cmd.sendResult(null, .{});
}
@@ -170,11 +164,15 @@ fn setCookies(cmd: anytype) !void {
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
- urls: []const []const u8,
+ urls: ?[]const []const u8 = null,
})) orelse return error.InvalidParams;
- var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, params.urls.len);
- for (params.urls) |url| {
+ // If not specified, use the URLs of the page and all of its subframes. TODO subframes
+ const page_url = if (bc.session.page) |*page| page.url.raw else null; // @speed: avoid repasing the URL
+ const param_urls = params.urls orelse &[_][]const u8{page_url orelse return error.InvalidParams};
+
+ var urls = try std.ArrayListUnmanaged(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len);
+ for (param_urls) |url| {
const uri = std.Uri.parse(url) catch return error.InvalidParams;
urls.appendAssumeCapacity(.{
@@ -354,3 +352,77 @@ test "cdp.network setExtraHTTPHeaders" {
try ctx.processMessage(.{ .id = 5, .method = "Target.attachToTarget", .params = .{ .targetId = bc.target_id.? } });
try testing.expectEqual(bc.cdp.extra_headers.items.len, 0);
}
+
+test "cdp.Network: cookies" {
+ const ResCookie = CdpStorage.ResCookie;
+ const CdpCookie = CdpStorage.CdpCookie;
+
+ var ctx = testing.context();
+ defer ctx.deinit();
+ _ = try ctx.loadBrowserContext(.{ .id = "BID-S" });
+
+ // Initially empty
+ try ctx.processMessage(.{
+ .id = 3,
+ .method = "Network.getCookies",
+ .params = .{ .urls = &[_][]const u8{"https://example.com/pancakes"} },
+ });
+ try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 3 });
+
+ // Has cookies after setting them
+ try ctx.processMessage(.{
+ .id = 4,
+ .method = "Network.setCookie",
+ .params = CdpCookie{ .name = "test3", .value = "valuenot3", .url = "https://car.example.com/defnotpancakes" },
+ });
+ try ctx.expectSentResult(null, .{ .id = 4 });
+ try ctx.processMessage(.{
+ .id = 5,
+ .method = "Network.setCookies",
+ .params = .{
+ .cookies = &[_]CdpCookie{
+ .{ .name = "test3", .value = "value3", .url = "https://car.example.com/pan/cakes" },
+ .{ .name = "test4", .value = "value4", .domain = "example.com", .path = "/mango" },
+ },
+ },
+ });
+ try ctx.expectSentResult(null, .{ .id = 5 });
+ try ctx.processMessage(.{
+ .id = 6,
+ .method = "Network.getCookies",
+ .params = .{ .urls = &[_][]const u8{"https://car.example.com/pan/cakes"} },
+ });
+ try ctx.expectSentResult(.{
+ .cookies = &[_]ResCookie{
+ .{ .name = "test3", .value = "value3", .domain = "car.example.com", .path = "/", .secure = true }, // No Pancakes!
+ },
+ }, .{ .id = 6 });
+
+ // deleteCookies
+ try ctx.processMessage(.{
+ .id = 7,
+ .method = "Network.deleteCookies",
+ .params = .{ .name = "test3", .domain = "car.example.com" },
+ });
+ try ctx.expectSentResult(null, .{ .id = 7 });
+ try ctx.processMessage(.{
+ .id = 8,
+ .method = "Storage.getCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ // Just the untouched test4 should be in the result
+ try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{.{ .name = "test4", .value = "value4", .domain = ".example.com", .path = "/mango" }} }, .{ .id = 8 });
+
+ // Empty after clearBrowserCookies
+ try ctx.processMessage(.{
+ .id = 9,
+ .method = "Network.clearBrowserCookies",
+ });
+ try ctx.expectSentResult(null, .{ .id = 9 });
+ try ctx.processMessage(.{
+ .id = 10,
+ .method = "Storage.getCookies",
+ .params = .{ .browserContextId = "BID-S" },
+ });
+ try ctx.expectSentResult(.{ .cookies = &[_]ResCookie{} }, .{ .id = 10 });
+}
From 0ed3fc0cde53d04952776ae9ffe3b34ea3d825cb Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Mon, 16 Jun 2025 11:54:53 +0200
Subject: [PATCH 13/14] JS may not set/get HttpOnly cookies
---
src/browser/html/document.zig | 8 +++++++-
src/browser/page.zig | 3 ++-
src/browser/storage/cookie.zig | 33 ++++++++++++++++++++++++++++-----
src/browser/xhr/xhr.zig | 1 +
src/cdp/domains/storage.zig | 7 +++----
5 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig
index 68483a56a..ed406ea55 100644
--- a/src/browser/html/document.zig
+++ b/src/browser/html/document.zig
@@ -81,7 +81,7 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .{};
- try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true });
+ try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false });
return buf.items;
}
@@ -90,6 +90,10 @@ pub const HTMLDocument = struct {
// outlives the page's arena.
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
errdefer c.deinit();
+ if (c.http_only) {
+ c.deinit();
+ return ""; // HttpOnly cookies cannot be set from JS
+ }
try page.cookie_jar.add(c, std.time.timestamp());
return cookie_str;
}
@@ -333,6 +337,8 @@ test "Browser.HTML.Document" {
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
+ .{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
+ .{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{});
try runner.testCases(&.{
diff --git a/src/browser/page.zig b/src/browser/page.zig
index 396022dba..23f90ea2b 100644
--- a/src/browser/page.zig
+++ b/src/browser/page.zig
@@ -217,7 +217,7 @@ pub const Page = struct {
{
// block exists to limit the lifetime of the request, which holds
// onto a connection
- var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true });
+ var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true, .is_http = true });
defer request.deinit();
request.body = opts.body;
@@ -498,6 +498,7 @@ pub const Page = struct {
var request = try self.newHTTPRequest(.GET, &url, .{
.origin_uri = &origin_url.uri,
.navigation = false,
+ .is_http = true,
});
defer request.deinit();
diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 1abd37e1c..eb9788261 100644
--- a/src/browser/storage/cookie.zig
+++ b/src/browser/storage/cookie.zig
@@ -12,6 +12,7 @@ pub const LookupOpts = struct {
request_time: ?i64 = null,
origin_uri: ?*const Uri = null,
navigation: bool = true,
+ is_http: bool,
};
pub const Jar = struct {
@@ -91,7 +92,7 @@ pub const Jar = struct {
var first = true;
for (self.cookies.items) |*cookie| {
- if (!cookie.appliesTo(&target, same_site, opts.navigation)) continue;
+ if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue;
// we have a match!
if (first) {
@@ -411,7 +412,12 @@ pub const Cookie = struct {
return .{ name, value, rest };
}
- pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool) bool {
+ pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool, is_http: bool) bool {
+ if (self.http_only and is_http == false) {
+ // http only cookies can be accessed from Javascript
+ return false;
+ }
+
if (url.secure == false and self.secure) {
// secure cookie can only be sent over HTTPs
return false;
@@ -581,7 +587,7 @@ test "Jar: forRequest" {
{
// test with no cookies
- try expectCookies("", &jar, test_uri, .{});
+ try expectCookies("", &jar, test_uri, .{ .is_http = true });
}
try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now);
@@ -595,97 +601,114 @@ test "Jar: forRequest" {
try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
// nothing fancy here
- try expectCookies("global1=1; global2=2", &jar, test_uri, .{});
- try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false });
+ try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true });
+ try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false, .is_http = true });
// We have a cookie where Domain=lightpanda.io
// This should _not_ match xyxlightpanda.io
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// matching path without trailing /
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// incomplete prefix path
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// path doesn't match
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// path doesn't match cookie directory
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// exact directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// sub directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// secure
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// navigational cross domain, secure
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
+ .is_http = true,
});
// navigational cross domain, insecure
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
+ .is_http = true,
});
// non-navigational cross domain, insecure
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
+ .is_http = true,
});
// non-navigational cross domain, secure
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false,
+ .is_http = true,
});
// non-navigational same origin
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
.navigation = false,
+ .is_http = true,
});
// exact domain match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// domain suffix match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
// non-matching domain
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
.origin_uri = &test_uri,
+ .is_http = true,
});
const l = jar.cookies.items.len;
try expectCookies("global1=1", &jar, test_uri, .{
.request_time = now + 100,
.origin_uri = &test_uri,
+ .is_http = true,
});
try testing.expectEqual(l - 1, jar.cookies.items.len);
diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig
index 37f87beed..e7b116309 100644
--- a/src/browser/xhr/xhr.zig
+++ b/src/browser/xhr/xhr.zig
@@ -475,6 +475,7 @@ pub const XMLHttpRequest = struct {
try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{
.navigation = false,
.origin_uri = &self.origin_url.uri,
+ .is_http = true,
});
if (arr.items.len > 0) {
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index cc26437b1..c2fb0d13b 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -133,8 +133,6 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
return error.NotYetImplementedParams;
}
- if (param.name.len == 0) return error.InvalidParams;
- if (param.value.len == 0) return error.InvalidParams;
var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
errdefer arena.deinit();
@@ -183,7 +181,7 @@ pub const CookieWriter = struct {
if (self.urls) |urls| {
for (self.cookies) |*cookie| {
for (urls) |*url| {
- if (cookie.appliesTo(url, true, true)) { // TBD same_site, should we compare to the pages url?
+ if (cookie.appliesTo(url, true, true, true)) { // TBD same_site, should we compare to the pages url?
try writeCookie(cookie, w);
break;
}
@@ -223,7 +221,8 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
try w.objectField("secure");
try w.write(cookie.secure);
- // TODO session
+ try w.objectField("session");
+ try w.write(cookie.expires == null);
try w.objectField("sameSite");
switch (cookie.same_site) {
From 19ec84854d588f17a23843ce8b69032c5c6f1384 Mon Sep 17 00:00:00 2001
From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com>
Date: Mon, 16 Jun 2025 17:50:04 +0200
Subject: [PATCH 14/14] Propagate set return
---
src/browser/html/document.zig | 2 +-
src/runtime/js.zig | 10 +++-------
2 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig
index ed406ea55..f1305bb83 100644
--- a/src/browser/html/document.zig
+++ b/src/browser/html/document.zig
@@ -337,7 +337,7 @@ test "Browser.HTML.Document" {
.{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" },
.{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
- .{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm
+ .{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", "" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" },
}, .{});
diff --git a/src/runtime/js.zig b/src/runtime/js.zig
index a86db0b5a..137a2c10f 100644
--- a/src/runtime/js.zig
+++ b/src/runtime/js.zig
@@ -2475,13 +2475,9 @@ fn Caller(comptime E: type, comptime State: type) type {
else => @compileError(named_function.full_name ++ " setter with more than 3 parameters, why?"),
}
- if (@typeInfo(Setter).@"fn".return_type) |return_type| {
- if (@typeInfo(return_type) == .error_union) {
- _ = try @call(.auto, func, args);
- return;
- }
- }
- _ = @call(.auto, func, args);
+ // TODO: Do not set res for void type to allow default input return behavior, but how to allow get to return undefined?
+ const res = @call(.auto, func, args);
+ info.getReturnValue().set(try js_context.zigValueToJs(res));
}
fn getIndex(self: *Self, comptime Struct: type, comptime named_function: NamedFunction, idx: u32, info: v8.PropertyCallbackInfo) !u8 {