diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 033ae8d5..645ac533 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -43,7 +43,7 @@ const Location = @import("../html/location.zig").Location; const storage = @import("../storage/storage.zig"); -const HttpClient = @import("../http/client.zig").Client; +const http = @import("../http/client.zig"); const UserContext = @import("../user_context.zig").UserContext; const polyfill = @import("../polyfill/polyfill.zig"); @@ -60,7 +60,7 @@ pub const Browser = struct { app: *App, session: ?*Session, allocator: Allocator, - http_client: *HttpClient, + http_client: *http.Client, session_pool: SessionPool, page_arena: std.heap.ArenaAllocator, @@ -130,10 +130,12 @@ pub const Session = struct { window: Window, - // TODO move the shed to the browser? + // TODO move the shed/jar to the browser? storage_shed: storage.Shed, + cookie_jar: storage.CookieJar, + page: ?Page = null, - http_client: *HttpClient, + http_client: *http.Client, jstypes: [Types.len]usize = undefined, @@ -148,6 +150,7 @@ pub const Session = struct { .http_client = browser.http_client, .storage_shed = storage.Shed.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator), + .cookie_jar = storage.CookieJar.init(allocator), .window = Window.create(null, .{ .agent = user_agent }), }; @@ -183,6 +186,7 @@ pub const Session = struct { } self.env.deinit(); self.arena.deinit(); + self.cookie_jar.deinit(); self.storage_shed.deinit(); } @@ -371,14 +375,16 @@ pub const Page = struct { } }); // load the data - var request = try self.session.http_client.request(.GET, self.uri); + var request = try self.newHTTPRequest(.GET, self.uri, .{ .navigation = true }); defer request.deinit(); - var response = try request.sendSync(.{}); + var response = try request.sendSync(.{}); const header = response.header; + try self.session.cookie_jar.populateFromResponse(self.uri, &header); + log.info("GET {any} {d}", .{ self.uri, header.status }); - const ct = response.header.get("content-type") orelse { + const ct = header.get("content-type") orelse { // no content type in HTTP headers. // TODO try to sniff mime type from the body. log.info("no content-type HTTP header", .{}); @@ -439,7 +445,9 @@ pub const Page = struct { // replace the user context document with the new one. try session.env.setUserContext(.{ + .uri = self.uri, .document = html_doc, + .cookie_jar = @ptrCast(&self.session.cookie_jar), .http_client = @ptrCast(self.session.http_client), }); @@ -614,13 +622,19 @@ pub const Page = struct { } const u = try std.Uri.resolve_inplace(self.uri, res_src, &b); - var request = try self.session.http_client.request(.GET, u); + var request = try self.newHTTPRequest(.GET, u, .{ + .origin_uri = self.uri, + .navigation = false, + }); defer request.deinit(); + var response = try request.sendSync(.{}); + var header = response.header; + try self.session.cookie_jar.populateFromResponse(u, &header); - log.info("fetch {any}: {d}", .{ u, response.header.status }); + log.info("fetch {any}: {d}", .{ u, header.status }); - if (response.header.status != 200) { + if (header.status != 200) { return FetchError.BadStatusCode; } @@ -645,6 +659,21 @@ pub const Page = struct { try s.eval(arena, &self.session.env, body); } + fn newHTTPRequest(self: *const Page, method: http.Request.Method, uri: std.Uri, opts: storage.cookie.LookupOpts) !http.Request { + const session = self.session; + var request = try session.http_client.request(method, uri); + errdefer request.deinit(); + + var arr: std.ArrayListUnmanaged(u8) = .{}; + try session.cookie_jar.forRequest(uri, arr.writer(self.arena), opts); + + if (arr.items.len > 0) { + try request.addHeader("Cookie", arr.items, .{}); + } + + return request; + } + const Script = struct { element: *parser.Element, kind: Kind, diff --git a/src/http/client.zig b/src/http/client.zig index ff1bfe9d..c447d935 100644 --- a/src/http/client.zig +++ b/src/http/client.zig @@ -1443,6 +1443,33 @@ pub const ResponseHeader = struct { pub fn count(self: *const ResponseHeader) usize { return self.headers.items.len; } + + pub fn iterate(self: *const ResponseHeader, name: []const u8) HeaderIterator { + return .{ + .index = 0, + .name = name, + .headers = self.headers, + }; + } +}; + +const HeaderIterator = struct { + index: usize, + name: []const u8, + headers: HeaderList, + + pub fn next(self: *HeaderIterator) ?[]const u8 { + const name = self.name; + const index = self.index; + for (self.headers.items[index..], index..) |h, i| { + if (std.mem.eql(u8, name, h.name)) { + self.index = i + 1; + return h.value; + } + } + self.index = self.headers.items.len; + return null; + } }; // What we emit from the AsyncHandler @@ -2044,6 +2071,52 @@ test "HttpClient: async redirect plaintext to TLS" { } } +test "HttpClient: HeaderIterator" { + var header = ResponseHeader{}; + defer header.headers.deinit(testing.allocator); + + { + var it = header.iterate("nope"); + try testing.expectEqual(null, it.next()); + try testing.expectEqual(null, it.next()); + } + + try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value1" }); + try header.headers.append(testing.allocator, .{ .name = "h2", .value = "value2" }); + try header.headers.append(testing.allocator, .{ .name = "h3", .value = "value3" }); + try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value4" }); + try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value5" }); + + { + var it = header.iterate("nope"); + try testing.expectEqual(null, it.next()); + try testing.expectEqual(null, it.next()); + } + + { + var it = header.iterate("h2"); + try testing.expectEqual("value2", it.next()); + try testing.expectEqual(null, it.next()); + try testing.expectEqual(null, it.next()); + } + + { + var it = header.iterate("h3"); + try testing.expectEqual("value3", it.next()); + try testing.expectEqual(null, it.next()); + try testing.expectEqual(null, it.next()); + } + + { + var it = header.iterate("h1"); + try testing.expectEqual("value1", it.next()); + try testing.expectEqual("value4", it.next()); + try testing.expectEqual("value5", it.next()); + try testing.expectEqual(null, it.next()); + try testing.expectEqual(null, it.next()); + } +} + const TestResponse = struct { status: u16, keepalive: ?bool, diff --git a/src/main_tests.zig b/src/main_tests.zig index 29bba51c..65bb4e3f 100644 --- a/src/main_tests.zig +++ b/src/main_tests.zig @@ -28,8 +28,7 @@ const apiweb = @import("apiweb.zig"); const Window = @import("html/window.zig").Window; const xhr = @import("xhr/xhr.zig"); const storage = @import("storage/storage.zig"); -const url = @import("url/url.zig"); -const URL = url.URL; +const URL = @import("url/url.zig").URL; const urlquery = @import("url/query.zig"); const Location = @import("html/location.zig").Location; @@ -54,7 +53,7 @@ const EventTestExecFn = @import("events/event.zig").testExecFn; const XHRTestExecFn = xhr.testExecFn; const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn; const StorageTestExecFn = storage.testExecFn; -const URLTestExecFn = url.testExecFn; +const URLTestExecFn = @import("url/url.zig").testExecFn; const HTMLElementTestExecFn = @import("html/elements.zig").testExecFn; const MutationObserverTestExecFn = @import("dom/mutation_observer.zig").testExecFn; @@ -91,16 +90,23 @@ fn testExecFn( var http_client = try @import("http/client.zig").Client.init(alloc, 5, .{}); defer http_client.deinit(); - try js_env.setUserContext(.{ - .document = doc, - .http_client = &http_client, - }); - // alias global as self and window var window = Window.create(null, null); - var u = try URL.constructor(alloc, "https://lightpanda.io/opensource-browser/", null); + const url = "https://lightpanda.io/opensource-browser/"; + var u = try URL.constructor(alloc, url, null); defer u.deinit(alloc); + + var cookie_jar = storage.CookieJar.init(alloc); + defer cookie_jar.deinit(); + + try js_env.setUserContext(.{ + .uri = try std.Uri.parse(url), + .document = doc, + .cookie_jar = &cookie_jar, + .http_client = &http_client, + }); + var location = Location{ .url = &u }; try window.replaceLocation(&location); diff --git a/src/storage/cookie.zig b/src/storage/cookie.zig index cfd31736..689a9f77 100644 --- a/src/storage/cookie.zig +++ b/src/storage/cookie.zig @@ -3,9 +3,14 @@ const Uri = std.Uri; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; +const http = @import("../http/client.zig"); const DateTime = @import("../datetime.zig").DateTime; const public_suffix_list = @import("../data/public_suffix_list.zig").lookup; +const log = std.log.scoped(.cookie); + +pub const LookupOpts = struct { request_time: ?i64 = null, origin_uri: ?Uri = null, navigation: bool = true }; + pub const Jar = struct { allocator: Allocator, cookies: std.ArrayListUnmanaged(Cookie), @@ -51,24 +56,19 @@ pub const Jar = struct { } } - pub fn forRequest( - self: *Jar, - allocator: Allocator, - request_time: i64, - origin_uri: ?Uri, - target_uri: Uri, - navigation: bool, - ) !CookieList { + pub fn forRequest(self: *Jar, target_uri: 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(origin_uri, target_host); + const same_site = try areSameSite(opts.origin_uri, target_host); const is_secure = std.mem.eql(u8, target_uri.scheme, "https"); - var matching: std.ArrayListUnmanaged(*Cookie) = .{}; - 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]; @@ -138,23 +138,74 @@ pub const Jar = struct { } } // we have a match! - try matching.append(allocator, cookie); + if (first) { + first = false; + } else { + try writer.writeAll(", "); + } + try writeCookie(cookie, writer); } + } - return .{ ._cookies = matching }; + pub fn populateFromResponse(self: *Jar, uri: Uri, header: *const http.ResponseHeader) !void { + const now = std.time.timestamp(); + var it = header.iterate("set-cookie"); + while (it.next()) |set_cookie| { + const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| { + log.warn("Couldn't parse cookie '{s}': {}\n", .{ set_cookie, err }); + continue; + }; + try self.add(c, now); + } + } + + 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(*Cookie), + _cookies: std.ArrayListUnmanaged(*const Cookie) = .{}, pub fn deinit(self: *CookieList, allocator: Allocator) void { self._cookies.deinit(allocator); } - pub fn cookies(self: *const CookieList) []*Cookie { + 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 { @@ -477,20 +528,11 @@ test "Jar: add" { test "Jar: forRequest" { const expectCookies = struct { - fn expect(expected: []const []const u8, list: *CookieList) !void { - defer list.deinit(testing.allocator); - const acutal_cookies = list._cookies.items; - - try testing.expectEqual(expected.len, acutal_cookies.len); - LOOP: for (expected) |e| { - for (acutal_cookies) |c| { - if (std.mem.eql(u8, e, c.name)) { - continue :LOOP; - } - } - std.debug.print("Cookie '{s}' not found", .{e}); - return error.CookieNotFound; - } + fn expect(expected: []const u8, jar: *Jar, target_uri: Uri, opts: LookupOpts) !void { + var arr: std.ArrayListUnmanaged(u8) = .{}; + defer arr.deinit(testing.allocator); + try jar.forRequest(target_uri, arr.writer(testing.allocator), opts); + try testing.expectEqual(expected, arr.items); } }.expect; @@ -503,8 +545,7 @@ test "Jar: forRequest" { { // test with no cookies - var matches = try jar.forRequest(testing.allocator, now, test_uri, test_uri, true); - try expectCookies(&.{}, &matches); + try expectCookies("", &jar, test_uri, .{}); } try jar.add(try Cookie.parse(testing.allocator, test_uri, "global1=1"), now); @@ -517,217 +558,139 @@ test "Jar: forRequest" { try jar.add(try Cookie.parse(testing.allocator, test_uri, "sitestrict=8;SameSite=Strict;Path=/x/"), now); try jar.add(try Cookie.parse(testing.allocator, test_uri_2, "domain1=9;domain=test.lightpanda.io"), now); - { - // nothing fancy here - var matches = try jar.forRequest(testing.allocator, now, test_uri, test_uri, true); - try expectCookies(&.{ "global1", "global2" }, &matches); - } + // 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 }); - { - // We have a cookie where Domain=lightpanda.io - // This should _not_ match xyxlightpanda.io - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://anothersitelightpanda.io/"), - true, - ); - try expectCookies(&.{}, &matches); - } + // 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, + }); - { - // matching path without trailing / - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/about"), - true, - ); - try expectCookies(&.{ "global1", "global2", "path1" }, &matches); - } + // 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, + }); - { - // incomplete prefix path - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/abou"), - true, - ); - try expectCookies(&.{ "global1", "global2" }, &matches); - } + // incomplete prefix path + try expectCookies("global1=1, global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{ + .origin_uri = test_uri, + }); - { - // path doesn't match - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/aboutus"), - true, - ); - try expectCookies(&.{ "global1", "global2" }, &matches); - } + // path doesn't match + try expectCookies("global1=1, global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{ + .origin_uri = test_uri, + }); - { - // path doesn't match cookie directory - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/docs"), - true, - ); - try expectCookies(&.{ "global1", "global2" }, &matches); - } + // 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, + }); - { - // exact directory match - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/docs/"), - true, - ); - try expectCookies(&.{ "global1", "global2", "path2" }, &matches); - } + // exact directory match + try expectCookies("global1=1, global2=2, path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{ + .origin_uri = test_uri, + }); - { - // sub directory match - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://lightpanda.io/docs/more"), - true, - ); - try expectCookies(&.{ "global1", "global2", "path2" }, &matches); - } + // 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, + }); - { - // secure - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("https://lightpanda.io/"), - true, - ); - try expectCookies(&.{ "global1", "global2", "secure" }, &matches); - } + // secure + try expectCookies("global1=1, global2=2, secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{ + .origin_uri = test_uri, + }); - { - // navigational cross domain, secure - var matches = try jar.forRequest( - testing.allocator, - now, - try std.Uri.parse("https://example.com/"), - try std.Uri.parse("https://lightpanda.io/x/"), - true, - ); - try expectCookies(&.{ "global1", "global2", "sitenone", "sitelax", "secure" }, &matches); - } + // 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/"), + }); - { - // navigational cross domain, insecure - var matches = try jar.forRequest( - testing.allocator, - now, - try std.Uri.parse("http://example.com/"), - try std.Uri.parse("http://lightpanda.io/x/"), - true, - ); - try expectCookies(&.{ "global1", "global2", "sitelax" }, &matches); - } + // 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/"), + }); - { - // non-navigational cross domain, insecure - var matches = try jar.forRequest( - testing.allocator, - now, - try std.Uri.parse("http://example.com/"), - try std.Uri.parse("http://lightpanda.io/x/"), - false, - ); - try expectCookies(&.{}, &matches); - } + // 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, + }); - { - // non-navigational cross domain, secure - var matches = try jar.forRequest( - testing.allocator, - now, - try std.Uri.parse("https://example.com/"), - try std.Uri.parse("https://lightpanda.io/x/"), - false, - ); - try expectCookies(&.{"sitenone"}, &matches); - } + // 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, + }); - { - // non-navigational same origin - var matches = try jar.forRequest( - testing.allocator, - now, - try std.Uri.parse("http://lightpanda.io/"), - try std.Uri.parse("http://lightpanda.io/x/"), - false, - ); - try expectCookies(&.{ "global1", "global2", "sitelax", "sitestrict" }, &matches); - } + // 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, + }); - { - // exact domain match + suffix - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://test.lightpanda.io/"), - true, - ); - try expectCookies(&.{ "global2", "domain1" }, &matches); - } + // exact domain match + suffix + try expectCookies("global2=2, domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{ + .origin_uri = test_uri, + }); - { - // domain suffix match + suffix - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://1.test.lightpanda.io/"), - true, - ); - try expectCookies(&.{ "global2", "domain1" }, &matches); - } + // domain suffix match + suffix + try expectCookies("global2=2, domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{ + .origin_uri = test_uri, + }); - { - // non-matching domain - var matches = try jar.forRequest( - testing.allocator, - now, - test_uri, - try std.Uri.parse("http://other.lightpanda.io/"), - true, - ); - try expectCookies(&.{"global2"}, &matches); - } + // non-matching domain + try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{ + .origin_uri = test_uri, + }); - { - // cookie has expired - const l = jar.cookies.items.len; - var matches = try jar.forRequest(testing.allocator, now + 100, test_uri, test_uri, true); - try expectCookies(&.{"global1"}, &matches); - try testing.expectEqual(l - 1, jar.cookies.items.len); - } + const l = jar.cookies.items.len; + try expectCookies("global1=1", &jar, test_uri, .{ + .request_time = now + 100, + .origin_uri = test_uri, + }); + try testing.expectEqual(l - 1, jar.cookies.items.len); // If you add more cases after this point, note that the above test removes // 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/storage/storage.zig b/src/storage/storage.zig index 53b5e42b..17fb8878 100644 --- a/src/storage/storage.zig +++ b/src/storage/storage.zig @@ -25,6 +25,10 @@ const DOMError = @import("netsurf").DOMError; const log = std.log.scoped(.storage); +pub const cookie = @import("cookie.zig"); +pub const Cookie = cookie.Cookie; +pub const CookieJar = cookie.Jar; + pub const Interfaces = .{ Bottle, }; diff --git a/src/user_context.zig b/src/user_context.zig index c2ca5971..e71b29f8 100644 --- a/src/user_context.zig +++ b/src/user_context.zig @@ -1,8 +1,11 @@ const std = @import("std"); const parser = @import("netsurf"); +const storage = @import("storage/storage.zig"); const Client = @import("http/client.zig").Client; pub const UserContext = struct { - document: *parser.DocumentHTML, http_client: *Client, + uri: std.Uri, + document: *parser.DocumentHTML, + cookie_jar: *storage.CookieJar, }; diff --git a/src/wpt/run.zig b/src/wpt/run.zig index 9989e36b..b152b5bd 100644 --- a/src/wpt/run.zig +++ b/src/wpt/run.zig @@ -58,10 +58,15 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const var http_client = try HttpClient.init(alloc, 2, .{}); defer http_client.deinit(); + var cookie_jar = storage.CookieJar.init(alloc); + defer cookie_jar.deinit(); + var js_env: Env = undefined; Env.init(&js_env, alloc, &loop, UserContext{ .document = html_doc, + .cookie_jar = &cookie_jar, .http_client = &http_client, + .uri = try std.Uri.parse("https://lightpanda.io"), }); defer js_env.deinit(); diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index da657897..6b3ed67a 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -35,6 +35,7 @@ const http = @import("../http/client.zig"); const parser = @import("netsurf"); +const CookieJar = @import("../storage/storage.zig").CookieJar; const UserContext = @import("../user_context.zig").UserContext; const log = std.log.scoped(.xhr); @@ -110,6 +111,10 @@ pub const XMLHttpRequest = struct { err: ?anyerror = null, last_dispatch: i64 = 0, + cookie_jar: *CookieJar, + // the URI of the page where this request is originating from + origin_uri: std.Uri, + // TODO uncomment this field causes casting issue with // XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but // not sure. see @@ -289,7 +294,9 @@ pub const XMLHttpRequest = struct { .url = null, .uri = undefined, .state = .unsent, + .origin_uri = userctx.uri, .client = userctx.http_client, + .cookie_jar = userctx.cookie_jar, }; } @@ -481,6 +488,18 @@ pub const XMLHttpRequest = struct { try request.addHeader(hdr.name, hdr.value, .{}); } + { + var arr: std.ArrayListUnmanaged(u8) = .{}; + try self.cookie_jar.forRequest(self.uri, arr.writer(alloc), .{ + .navigation = false, + .origin_uri = self.origin_uri, + }); + + if (arr.items.len > 0) { + try request.addHeader("Cookie", arr.items, .{}); + } + } + // The body argument provides the request body, if any, and is ignored // if the request method is GET or HEAD. // https://xhr.spec.whatwg.org/#the-send()-method @@ -526,6 +545,8 @@ pub const XMLHttpRequest = struct { self.state = .loading; self.dispatchEvt("readystatechange"); + + try self.cookie_jar.populateFromResponse(self.uri, &header); } if (progress.data) |data| {