diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig
index 55cbc6c6..aa93ba21 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 30b05499..ed3388a4 100644
--- a/src/cdp/cdp.zig
+++ b/src/cdp/cdp.zig
@@ -203,6 +203,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
asUint(u56, "Browser") => return @import("domains/browser.zig").processMessage(command),
asUint(u56, "Runtime") => return @import("domains/runtime.zig").processMessage(command),
asUint(u56, "Network") => return @import("domains/network.zig").processMessage(command),
+ asUint(u56, "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 55cff246..c7ad0c60 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 00000000..654d976b
--- /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();
+ }
+};