Add support for XHR's withCredentials

XHR should only send and receive cookies for same-origin requests or if
withCredentials is true.
This commit is contained in:
Karl Seguin
2026-02-07 16:16:10 +08:00
parent aca3fae6b1
commit cecdf0d511
3 changed files with 57 additions and 29 deletions

View File

@@ -197,6 +197,7 @@ pub const Accessor = struct {
cache: ?[]const u8 = null, cache: ?[]const u8 = null,
as_typed_array: bool = false, as_typed_array: bool = false,
null_as_undefined: bool = false, null_as_undefined: bool = false,
dom_exception: bool = false,
}; };
fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor { fn init(comptime T: type, comptime getter: anytype, comptime setter: anytype, comptime opts: Opts) Accessor {
@@ -215,12 +216,14 @@ pub const Accessor = struct {
if (comptime opts.static) { if (comptime opts.static) {
caller.function(T, getter, handle.?, .{ caller.function(T, getter, handle.?, .{
.cache = opts.cache, .cache = opts.cache,
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });
} else { } else {
caller.method(T, getter, handle.?, .{ caller.method(T, getter, handle.?, .{
.cache = opts.cache, .cache = opts.cache,
.dom_exception = opts.dom_exception,
.as_typed_array = opts.as_typed_array, .as_typed_array = opts.as_typed_array,
.null_as_undefined = opts.null_as_undefined, .null_as_undefined = opts.null_as_undefined,
}); });

View File

@@ -56,6 +56,7 @@ _response_type: ResponseType = .text,
_ready_state: ReadyState = .unsent, _ready_state: ReadyState = .unsent,
_on_ready_state_change: ?js.Function.Temp = null, _on_ready_state_change: ?js.Function.Temp = null,
_with_credentials: bool = false,
const ReadyState = enum(u8) { const ReadyState = enum(u8) {
unsent = 0, unsent = 0,
@@ -150,6 +151,17 @@ pub fn setOnReadyStateChange(self: *XMLHttpRequest, cb_: ?js.Function) !void {
} }
} }
pub fn getWithCredentials(self: *const XMLHttpRequest) bool {
return self._with_credentials;
}
pub fn setWithCredentials(self: *XMLHttpRequest, value: bool) !void {
if (self._ready_state != .unsent and self._ready_state != .opened) {
return error.InvalidStateError;
}
self._with_credentials = value;
}
// TODO: this takes an optional 3 more parameters // TODO: this takes an optional 3 more parameters
// TODO: url should be a union, as it can be multiple things // TODO: url should be a union, as it can be multiple things
pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void { pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void {
@@ -198,8 +210,14 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
const page = self._page; const page = self._page;
const http_client = page._session.browser.http_client; const http_client = page._session.browser.http_client;
var headers = try http_client.newHeaders(); var headers = try http_client.newHeaders();
// Only add cookies for same-origin or when withCredentials is true
const cookie_support = self._with_credentials or try page.isSameOrigin(self._url);
try self._request_headers.populateHttpHeader(page.call_arena, &headers); try self._request_headers.populateHttpHeader(page.call_arena, &headers);
try page.headersForRequest(self._arena, self._url, &headers); if (cookie_support) {
try page.headersForRequest(self._arena, self._url, &headers);
}
try http_client.request(.{ try http_client.request(.{
.ctx = self, .ctx = self,
@@ -207,7 +225,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.method = self._method, .method = self._method,
.headers = headers, .headers = headers,
.body = self._request_body, .body = self._request_body,
.cookie_jar = &page._session.cookie_jar, .cookie_jar = if (cookie_support) &page._session.cookie_jar else null,
.resource_type = .xhr, .resource_type = .xhr,
.notification = page._session.notification, .notification = page._session.notification,
.start_callback = httpStartCallback, .start_callback = httpStartCallback,
@@ -541,6 +559,7 @@ pub const JsApi = struct {
pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done)); pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done));
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{}); pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });
pub const open = bridge.function(XMLHttpRequest.open, .{}); pub const open = bridge.function(XMLHttpRequest.open, .{});
pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true }); pub const send = bridge.function(XMLHttpRequest.send, .{ .dom_exception = true });
pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{}); pub const responseType = bridge.accessor(XMLHttpRequest.getResponseType, XMLHttpRequest.setResponseType, .{});

View File

@@ -783,7 +783,7 @@ pub const Request = struct {
url: [:0]const u8, url: [:0]const u8,
headers: Http.Headers, headers: Http.Headers,
body: ?[]const u8 = null, body: ?[]const u8 = null,
cookie_jar: *CookieJar, cookie_jar: ?*CookieJar,
resource_type: ResourceType, resource_type: ResourceType,
credentials: ?[:0]const u8 = null, credentials: ?[:0]const u8 = null,
notification: *Notification, notification: *Notification,
@@ -1057,13 +1057,15 @@ pub const Transfer = struct {
const arena = transfer.arena.allocator(); const arena = transfer.arena.allocator();
// retrieve cookies from the redirect's response. // retrieve cookies from the redirect's response.
var i: usize = 0; if (req.cookie_jar) |jar| {
while (true) { var i: usize = 0;
const ct = getResponseHeader(easy, "set-cookie", i); while (true) {
if (ct == null) break; const ct = getResponseHeader(easy, "set-cookie", i);
try req.cookie_jar.populateFromResponse(transfer.url, ct.?.value); if (ct == null) break;
i += 1; try jar.populateFromResponse(transfer.url, ct.?.value);
if (i >= ct.?.amount) break; i += 1;
if (i >= ct.?.amount) break;
}
} }
// set cookies for the following redirection's request. // set cookies for the following redirection's request.
@@ -1077,15 +1079,17 @@ pub const Transfer = struct {
const url = try URL.resolve(arena, std.mem.span(base_url), location.value, .{}); const url = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
transfer.url = url; transfer.url = url;
var cookies: std.ArrayList(u8) = .{}; if (req.cookie_jar) |jar| {
try req.cookie_jar.forRequest(url, cookies.writer(arena), .{ var cookies: std.ArrayList(u8) = .{};
.is_http = true, try jar.forRequest(url, cookies.writer(arena), .{
.origin_url = url, .is_http = true,
// used to enforce samesite cookie rules .origin_url = url,
.is_navigation = req.resource_type == .document, // used to enforce samesite cookie rules
}); .is_navigation = req.resource_type == .document,
try cookies.append(arena, 0); //null terminate });
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COOKIE, @as([*c]const u8, @ptrCast(cookies.items.ptr)))); try cookies.append(arena, 0); //null terminate
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COOKIE, @as([*c]const u8, @ptrCast(cookies.items.ptr))));
}
} }
// headerDoneCallback is called once the headers have been read. // headerDoneCallback is called once the headers have been read.
@@ -1107,16 +1111,18 @@ pub const Transfer = struct {
@memcpy(hdr._content_type[0..len], value[0..len]); @memcpy(hdr._content_type[0..len], value[0..len]);
} }
var i: usize = 0; if (transfer.req.cookie_jar) |jar| {
while (true) { var i: usize = 0;
const ct = getResponseHeader(easy, "set-cookie", i); while (true) {
if (ct == null) break; const ct = getResponseHeader(easy, "set-cookie", i);
transfer.req.cookie_jar.populateFromResponse(transfer.url, ct.?.value) catch |err| { if (ct == null) break;
log.err(.http, "set cookie", .{ .err = err, .req = transfer }); jar.populateFromResponse(transfer.url, ct.?.value) catch |err| {
return err; log.err(.http, "set cookie", .{ .err = err, .req = transfer });
}; return err;
i += 1; };
if (i >= ct.?.amount) break; i += 1;
if (i >= ct.?.amount) break;
}
} }
const proceed = transfer.req.header_callback(transfer) catch |err| { const proceed = transfer.req.header_callback(transfer) catch |err| {