Skip to content

Commit ab97475

Browse files
committed
Add cookie support to browser (not XHR yet) requests
1 parent afdb5d7 commit ab97475

File tree

4 files changed

+202
-13
lines changed

4 files changed

+202
-13
lines changed

src/browser/browser.zig

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const Location = @import("../html/location.zig").Location;
4343

4444
const storage = @import("../storage/storage.zig");
4545

46-
const HttpClient = @import("../http/client.zig").Client;
46+
const http = @import("../http/client.zig");
4747
const UserContext = @import("../user_context.zig").UserContext;
4848

4949
const polyfill = @import("../polyfill/polyfill.zig");
@@ -60,7 +60,7 @@ pub const Browser = struct {
6060
app: *App,
6161
session: ?*Session,
6262
allocator: Allocator,
63-
http_client: *HttpClient,
63+
http_client: *http.Client,
6464
session_pool: SessionPool,
6565
page_arena: std.heap.ArenaAllocator,
6666

@@ -123,10 +123,12 @@ pub const Session = struct {
123123

124124
window: Window,
125125

126-
// TODO move the shed to the browser?
126+
// TODO move the shed/jar to the browser?
127127
storage_shed: storage.Shed,
128+
cookie_jar: storage.CookieJar,
129+
128130
page: ?Page = null,
129-
http_client: *HttpClient,
131+
http_client: *http.Client,
130132

131133
jstypes: [Types.len]usize = undefined,
132134

@@ -141,6 +143,7 @@ pub const Session = struct {
141143
.http_client = browser.http_client,
142144
.storage_shed = storage.Shed.init(allocator),
143145
.arena = std.heap.ArenaAllocator.init(allocator),
146+
.cookie_jar = storage.CookieJar.init(allocator),
144147
.window = Window.create(null, .{ .agent = user_agent }),
145148
};
146149

@@ -176,6 +179,7 @@ pub const Session = struct {
176179
}
177180
self.env.deinit();
178181
self.arena.deinit();
182+
self.cookie_jar.deinit();
179183
self.storage_shed.deinit();
180184
}
181185

@@ -364,14 +368,16 @@ pub const Page = struct {
364368
} });
365369

366370
// load the data
367-
var request = try self.session.http_client.request(.GET, self.uri);
371+
var request = try self.newHTTPRequest(.GET, self.uri, .{ .navigation = true });
368372
defer request.deinit();
369-
var response = try request.sendSync(.{});
370373

374+
var response = try request.sendSync(.{});
371375
const header = response.header;
376+
try self.processHTTPResponse(self.uri, &header);
377+
372378
log.info("GET {any} {d}", .{ self.uri, header.status });
373379

374-
const ct = response.header.get("content-type") orelse {
380+
const ct = header.get("content-type") orelse {
375381
// no content type in HTTP headers.
376382
// TODO try to sniff mime type from the body.
377383
log.info("no content-type HTTP header", .{});
@@ -607,13 +613,19 @@ pub const Page = struct {
607613
}
608614
const u = try std.Uri.resolve_inplace(self.uri, res_src, &b);
609615

610-
var request = try self.session.http_client.request(.GET, u);
616+
var request = try self.newHTTPRequest(.GET, u, .{
617+
.origin = self.uri,
618+
.navigation = false,
619+
});
611620
defer request.deinit();
621+
612622
var response = try request.sendSync(.{});
623+
var header = response.header;
624+
try self.processHTTPResponse(u, &header);
613625

614-
log.info("fetch {any}: {d}", .{ u, response.header.status });
626+
log.info("fetch {any}: {d}", .{ u, header.status });
615627

616-
if (response.header.status != 200) {
628+
if (header.status != 200) {
617629
return FetchError.BadStatusCode;
618630
}
619631

@@ -638,6 +650,46 @@ pub const Page = struct {
638650
try s.eval(arena, &self.session.env, body);
639651
}
640652

653+
const RequestOpts = struct {
654+
origin: ?std.Uri = null,
655+
navigation: bool = true,
656+
};
657+
fn newHTTPRequest(self: *const Page, method: http.Request.Method, uri: std.Uri, opts: RequestOpts) !http.Request {
658+
const session = self.session;
659+
var request = try session.http_client.request(method, uri);
660+
errdefer request.deinit();
661+
662+
var cookies = try session.cookie_jar.forRequest(
663+
self.arena,
664+
std.time.timestamp(),
665+
opts.origin,
666+
uri,
667+
opts.navigation,
668+
);
669+
defer cookies.deinit(self.arena);
670+
671+
if (cookies.len() > 0) {
672+
var arr: std.ArrayListUnmanaged(u8) = .{};
673+
try cookies.write(arr.writer(self.arena));
674+
try request.addHeader("Cookie", arr.items, .{});
675+
}
676+
677+
return request;
678+
}
679+
680+
fn processHTTPResponse(self: *const Page, uri: std.Uri, header: *const http.ResponseHeader) !void {
681+
const session = self.session;
682+
const now = std.time.timestamp();
683+
var it = header.iterate("set-cookie");
684+
while (it.next()) |set_cookie| {
685+
const c = storage.Cookie.parse(self.arena, uri, set_cookie) catch |err| {
686+
log.warn("Couldn't parse cookie '{s}': {}\n", .{ set_cookie, err });
687+
continue;
688+
};
689+
try session.cookie_jar.add(c, now);
690+
}
691+
}
692+
641693
const Script = struct {
642694
element: *parser.Element,
643695
kind: Kind,

src/http/client.zig

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,33 @@ pub const ResponseHeader = struct {
14301430
pub fn count(self: *const ResponseHeader) usize {
14311431
return self.headers.items.len;
14321432
}
1433+
1434+
pub fn iterate(self: *const ResponseHeader, name: []const u8) HeaderIterator {
1435+
return .{
1436+
.index = 0,
1437+
.name = name,
1438+
.headers = self.headers,
1439+
};
1440+
}
1441+
};
1442+
1443+
const HeaderIterator = struct {
1444+
index: usize,
1445+
name: []const u8,
1446+
headers: HeaderList,
1447+
1448+
pub fn next(self: *HeaderIterator) ?[]const u8 {
1449+
const name = self.name;
1450+
const index = self.index;
1451+
for (self.headers.items[index..], index..) |h, i| {
1452+
if (std.mem.eql(u8, name, h.name)) {
1453+
self.index = i + 1;
1454+
return h.value;
1455+
}
1456+
}
1457+
self.index = self.headers.items.len;
1458+
return null;
1459+
}
14331460
};
14341461

14351462
// What we emit from the AsyncHandler
@@ -2031,6 +2058,52 @@ test "HttpClient: async redirect plaintext to TLS" {
20312058
}
20322059
}
20332060

2061+
test "HttpClient: HeaderIterator" {
2062+
var header = ResponseHeader{};
2063+
defer header.headers.deinit(testing.allocator);
2064+
2065+
{
2066+
var it = header.iterate("nope");
2067+
try testing.expectEqual(null, it.next());
2068+
try testing.expectEqual(null, it.next());
2069+
}
2070+
2071+
try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value1" });
2072+
try header.headers.append(testing.allocator, .{ .name = "h2", .value = "value2" });
2073+
try header.headers.append(testing.allocator, .{ .name = "h3", .value = "value3" });
2074+
try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value4" });
2075+
try header.headers.append(testing.allocator, .{ .name = "h1", .value = "value5" });
2076+
2077+
{
2078+
var it = header.iterate("nope");
2079+
try testing.expectEqual(null, it.next());
2080+
try testing.expectEqual(null, it.next());
2081+
}
2082+
2083+
{
2084+
var it = header.iterate("h2");
2085+
try testing.expectEqual("value2", it.next());
2086+
try testing.expectEqual(null, it.next());
2087+
try testing.expectEqual(null, it.next());
2088+
}
2089+
2090+
{
2091+
var it = header.iterate("h3");
2092+
try testing.expectEqual("value3", it.next());
2093+
try testing.expectEqual(null, it.next());
2094+
try testing.expectEqual(null, it.next());
2095+
}
2096+
2097+
{
2098+
var it = header.iterate("h1");
2099+
try testing.expectEqual("value1", it.next());
2100+
try testing.expectEqual("value4", it.next());
2101+
try testing.expectEqual("value5", it.next());
2102+
try testing.expectEqual(null, it.next());
2103+
try testing.expectEqual(null, it.next());
2104+
}
2105+
}
2106+
20342107
const TestResponse = struct {
20352108
status: u16,
20362109
keepalive: ?bool,

src/storage/cookie.zig

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pub const Jar = struct {
6565
const same_site = try areSameSite(origin_uri, target_host);
6666
const is_secure = std.mem.eql(u8, target_uri.scheme, "https");
6767

68-
var matching: std.ArrayListUnmanaged(*Cookie) = .{};
68+
var matching: std.ArrayListUnmanaged(*const Cookie) = .{};
6969

7070
var i: usize = 0;
7171
var cookies = self.cookies.items;
@@ -146,15 +146,41 @@ pub const Jar = struct {
146146
};
147147

148148
pub const CookieList = struct {
149-
_cookies: std.ArrayListUnmanaged(*Cookie),
149+
_cookies: std.ArrayListUnmanaged(*const Cookie) = .{},
150150

151151
pub fn deinit(self: *CookieList, allocator: Allocator) void {
152152
self._cookies.deinit(allocator);
153153
}
154154

155-
pub fn cookies(self: *const CookieList) []*Cookie {
155+
pub fn cookies(self: *const CookieList) []*const Cookie {
156156
return self._cookies.items;
157157
}
158+
159+
pub fn len(self: *const CookieList) usize {
160+
return self._cookies.items.len;
161+
}
162+
163+
pub fn write(self: *const CookieList, writer: anytype) !void {
164+
const all = self._cookies.items;
165+
if (all.len == 0) {
166+
return;
167+
}
168+
try writeCookie(all[0], writer);
169+
for (all[1..]) |cookie| {
170+
try writer.writeAll("; ");
171+
try writeCookie(cookie, writer);
172+
}
173+
}
174+
175+
fn writeCookie(cookie: *const Cookie, writer: anytype) !void {
176+
if (cookie.name.len > 0) {
177+
try writer.writeAll(cookie.name);
178+
try writer.writeByte('=');
179+
}
180+
if (cookie.value.len > 0) {
181+
try writer.writeAll(cookie.value);
182+
}
183+
}
158184
};
159185

160186
fn isCookieExpired(cookie: *const Cookie, now: i64) bool {
@@ -728,6 +754,40 @@ test "Jar: forRequest" {
728754
// the 'global2' cookie
729755
}
730756

757+
test "CookieList: write" {
758+
var arr: std.ArrayListUnmanaged(u8) = .{};
759+
defer arr.deinit(testing.allocator);
760+
761+
var cookie_list = CookieList{};
762+
defer cookie_list.deinit(testing.allocator);
763+
764+
const c1 = try Cookie.parse(testing.allocator, test_uri, "cookie_name=cookie_value");
765+
defer c1.deinit();
766+
{
767+
try cookie_list._cookies.append(testing.allocator, &c1);
768+
try cookie_list.write(arr.writer(testing.allocator));
769+
try testing.expectEqual("cookie_name=cookie_value", arr.items);
770+
}
771+
772+
const c2 = try Cookie.parse(testing.allocator, test_uri, "x84");
773+
defer c2.deinit();
774+
{
775+
arr.clearRetainingCapacity();
776+
try cookie_list._cookies.append(testing.allocator, &c2);
777+
try cookie_list.write(arr.writer(testing.allocator));
778+
try testing.expectEqual("cookie_name=cookie_value; x84", arr.items);
779+
}
780+
781+
const c3 = try Cookie.parse(testing.allocator, test_uri, "nope=");
782+
defer c3.deinit();
783+
{
784+
arr.clearRetainingCapacity();
785+
try cookie_list._cookies.append(testing.allocator, &c3);
786+
try cookie_list.write(arr.writer(testing.allocator));
787+
try testing.expectEqual("cookie_name=cookie_value; x84; nope=", arr.items);
788+
}
789+
}
790+
731791
test "Cookie: parse key=value" {
732792
try expectError(error.Empty, null, "");
733793
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });

src/storage/storage.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ const DOMError = @import("netsurf").DOMError;
2525

2626
const log = std.log.scoped(.storage);
2727

28+
const cookie = @import("cookie.zig");
29+
pub const Cookie = cookie.Cookie;
30+
pub const CookieJar = cookie.Jar;
31+
2832
pub const Interfaces = .{
2933
Bottle,
3034
};

0 commit comments

Comments
 (0)