diff --git a/src/TestWSServer.zig b/src/TestWSServer.zig
index 6d28acf3..35e9e29e 100644
--- a/src/TestWSServer.zig
+++ b/src/TestWSServer.zig
@@ -95,6 +95,22 @@ fn handleClient(client: posix.socket_t) void {
const key_end = std.mem.indexOfScalarPos(u8, request, key_line_start, '\r') orelse return;
const key = request[key_line_start..key_end];
+ // Capture the request's Cookie header value (if any) so the test
+ // fixture can ask for it back via the `get-cookie` command. Copy out
+ // of `request` because the WS frame loop below reuses `buf` for
+ // incoming frames, invalidating the slice. Buffer is sized to match
+ // `buf` since any cookie that fit in the request fits here.
+ var cookie_buf: [4096]u8 = undefined;
+ var cookie_len: usize = 0;
+ const cookie_header = "Cookie: ";
+ if (std.mem.indexOf(u8, request, cookie_header)) |cookie_start| {
+ const value_start = cookie_start + cookie_header.len;
+ const value_end = std.mem.indexOfScalarPos(u8, request, value_start, '\r') orelse value_start;
+ const value = request[value_start..value_end];
+ cookie_len = @min(value.len, cookie_buf.len);
+ @memcpy(cookie_buf[0..cookie_len], value[0..cookie_len]);
+ }
+
// Compute accept key
var hasher = std.crypto.hash.Sha1.init(.{});
hasher.update(key);
@@ -128,7 +144,7 @@ fn handleClient(client: posix.socket_t) void {
// Handle commands or echo
if (frame.opcode == 1) { // Text
- handleTextMessage(client, frame.payload) catch break;
+ handleTextMessage(client, frame.payload, cookie_buf[0..cookie_len]) catch break;
} else if (frame.opcode == 2) { // Binary
handleBinaryMessage(client, frame.payload) catch break;
}
@@ -233,12 +249,20 @@ const RecvBuffer = struct {
}
};
-fn handleTextMessage(client: posix.socket_t, payload: []const u8) !void {
+fn handleTextMessage(client: posix.socket_t, payload: []const u8, cookie_header: []const u8) !void {
// Command: force-close - close socket immediately without close frame
if (std.mem.eql(u8, payload, "force-close")) {
return error.ForceClose;
}
+ // Command: get-cookie - send the Cookie header value the upgrade
+ // request carried (empty string if none). Used by the cookie-on-
+ // upgrade regression test.
+ if (std.mem.eql(u8, payload, "get-cookie")) {
+ try sendFrame(client, 1, "cookie:", cookie_header);
+ return;
+ }
+
// Command: send-large:N - send a message of N bytes
if (std.mem.startsWith(u8, payload, "send-large:")) {
const size_str = payload["send-large:".len..];
diff --git a/src/browser/HttpClient.zig b/src/browser/HttpClient.zig
index 81c4dc14..6eb3611e 100644
--- a/src/browser/HttpClient.zig
+++ b/src/browser/HttpClient.zig
@@ -1247,10 +1247,12 @@ pub const Request = struct {
.origin_url = self.cookie_origin,
.is_navigation = self.resource_type == .document,
});
- const written = aw.written();
- if (written.len == 0) return null;
+ if (aw.written().len == 0) {
+ return null;
+ }
try aw.writer.writeByte(0);
- return written.ptr[0..written.len :0];
+ const written = aw.written();
+ return written.ptr[0 .. written.len - 1 :0];
}
pub fn deinit(self: *const Request) void {
diff --git a/src/browser/URL.zig b/src/browser/URL.zig
index 620e63a2..f2650164 100644
--- a/src/browser/URL.zig
+++ b/src/browser/URL.zig
@@ -464,8 +464,8 @@ pub fn getProtocol(raw: [:0]const u8) []const u8 {
return raw[0 .. pos + 1];
}
-pub fn isHTTPS(raw: [:0]const u8) bool {
- return std.mem.startsWith(u8, raw, "https:");
+pub fn isSecure(raw: [:0]const u8) bool {
+ return std.mem.startsWith(u8, raw, "https:") or std.mem.startsWith(u8, raw, "wss:");
}
pub fn getHostname(raw: [:0]const u8) []const u8 {
diff --git a/src/browser/tests/net/websocket.html b/src/browser/tests/net/websocket.html
index 2c2e315b..e85813b2 100644
--- a/src/browser/tests/net/websocket.html
+++ b/src/browser/tests/net/websocket.html
@@ -579,3 +579,169 @@
});
}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/net/WebSocket.zig b/src/browser/webapi/net/WebSocket.zig
index 4d2b589f..c80b520f 100644
--- a/src/browser/webapi/net/WebSocket.zig
+++ b/src/browser/webapi/net/WebSocket.zig
@@ -133,9 +133,29 @@ pub fn init(url: []const u8, protocols: [][]const u8, exec: *const Execution) !*
var headers = try http_client.newHeaders();
errdefer headers.deinit();
+ var has_extra_headers = false;
+
if (protocols.len > 0) {
const header = try std.fmt.allocPrintSentinel(arena, "Sec-WebSocket-Protocol: {s}", .{try std.mem.join(arena, ", ", protocols)}, 0);
try headers.add(header);
+ has_extra_headers = true;
+ }
+
+ {
+ var buf: std.Io.Writer.Allocating = .init(arena);
+ try exec.session.cookie_jar.forRequest(resolved_url, &buf.writer, .{
+ .is_http = true,
+ .is_navigation = false,
+ .origin_url = exec.url.*,
+ });
+ if (buf.written().len > 0) {
+ try buf.writer.writeByte(0);
+ const written = buf.written();
+ try conn.setCookies(written.ptr[0 .. written.len - 1 :0]);
+ }
+ }
+
+ if (has_extra_headers) {
try conn.setHeaders(&headers);
}
diff --git a/src/browser/webapi/storage/Cookie.zig b/src/browser/webapi/storage/Cookie.zig
index 0e023705..b7c721ea 100644
--- a/src/browser/webapi/storage/Cookie.zig
+++ b/src/browser/webapi/storage/Cookie.zig
@@ -577,7 +577,7 @@ pub const Jar = struct {
const target = PreparedUri{
.host = URL.getHostname(target_url),
.path = URL.getPathname(target_url),
- .secure = URL.isHTTPS(target_url),
+ .secure = URL.isSecure(target_url),
};
const same_site = try areSameSite(opts.origin_url, target.host);
diff --git a/src/browser/webapi/storage/CookieStore.zig b/src/browser/webapi/storage/CookieStore.zig
index 4d679e54..cf365537 100644
--- a/src/browser/webapi/storage/CookieStore.zig
+++ b/src/browser/webapi/storage/CookieStore.zig
@@ -76,7 +76,7 @@ fn onCookieChanged(ctx: *anyopaque, data: *const Notification.CookieChanged) !vo
const target = Cookie.PreparedUri{
.host = URL.getHostname(doc_url),
.path = URL.getPathname(doc_url),
- .secure = URL.isHTTPS(doc_url),
+ .secure = URL.isSecure(doc_url),
};
if (target.host.len == 0) return;
@@ -353,7 +353,7 @@ fn matchCookies(
const target = Cookie.PreparedUri{
.host = URL.getHostname(url_resolved),
.path = URL.getPathname(url_resolved),
- .secure = URL.isHTTPS(url_resolved),
+ .secure = URL.isSecure(url_resolved),
};
if (target.host.len == 0) return error.SecurityError;
@@ -408,7 +408,7 @@ fn storeCookie(exec: *const Execution, init: CookieInit) !void {
if (std.mem.indexOfAny(u8, d, ";\r\n\x00") != null) return error.InvalidCookieDomain;
}
- const is_https = URL.isHTTPS(url);
+ const is_https = URL.isSecure(url);
// Per spec, SameSite=None requires Secure. CookieStore additionally
// marks any cookie written from an HTTPS document as Secure.
const secure = is_https or init.sameSite == .none;
diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig
index 5a33df88..864c72f5 100644
--- a/src/cdp/domains/network.zig
+++ b/src/cdp/domains/network.zig
@@ -233,7 +233,7 @@ fn getCookies(cmd: *CDP.Command) !void {
urls.appendAssumeCapacity(.{
.host = try Cookie.parseDomain(cmd.arena, url, null),
.path = try Cookie.parsePath(cmd.arena, url, null),
- .secure = URL.isHTTPS(url),
+ .secure = URL.isSecure(url),
});
}
diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig
index dac20e10..7e38c132 100644
--- a/src/cdp/domains/storage.zig
+++ b/src/cdp/domains/storage.zig
@@ -153,7 +153,7 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void {
const domain = try Cookie.parseDomain(a, param.url, param.domain);
const path = if (param.path == null) "/" else try Cookie.parsePath(a, null, param.path);
- const secure = if (param.secure) |s| s else if (param.url) |url| URL.isHTTPS(url) else false;
+ const secure = if (param.secure) |s| s else if (param.url) |url| URL.isSecure(url) else false;
break :blk Cookie{
.arena = arena,