Skip to content

Commit b7ed897

Browse files
committed
enable curl cookie engine
Enabling Curl cookie engine brings advantages: * handle cookies during a redirection: when a srv redirects including cookies, curl sends back the cookies correctly during the next request * benefit curl's cookie parsing: we now use curl's lib to parse cookies instead of parsing them from headers manually
1 parent 77eee7f commit b7ed897

File tree

3 files changed

+118
-12
lines changed

3 files changed

+118
-12
lines changed

src/browser/storage/cookie.zig

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,17 @@ pub const Jar = struct {
109109
}
110110
}
111111

112+
// https://curl.se/docs/http-cookies.html
113+
pub fn populateFromCurl(self: *Jar, set_cookie: []const u8) !void {
114+
const c = Cookie.parseCurl(self.allocator, set_cookie) catch |err| {
115+
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
116+
return;
117+
};
118+
119+
const now = std.time.timestamp();
120+
try self.add(c, now);
121+
}
122+
112123
pub fn populateFromResponse(self: *Jar, uri: *const Uri, set_cookie: []const u8) !void {
113124
const c = Cookie.parse(self.allocator, uri, set_cookie) catch |err| {
114125
log.warn(.web_api, "cookie parse failed", .{ .raw = set_cookie, .err = err });
@@ -192,6 +203,62 @@ pub const Cookie = struct {
192203
self.arena.deinit();
193204
}
194205

206+
// Parse curl's cookie file format
207+
// https://curl.se/docs/http-cookies.html
208+
pub fn parseCurl(allocator: Allocator, str: []const u8) !Cookie {
209+
var c: Cookie = .{
210+
.arena = ArenaAllocator.init(allocator),
211+
.name = undefined,
212+
.value = undefined,
213+
.path = undefined,
214+
.same_site = undefined,
215+
.secure = undefined,
216+
.http_only = undefined,
217+
.domain = undefined,
218+
.expires = undefined,
219+
};
220+
errdefer c.deinit();
221+
222+
var aa = c.arena.allocator();
223+
224+
var it = std.mem.splitScalar(u8, str, '\t');
225+
var index: u8 = 0;
226+
while (it.next()) |v| {
227+
defer index += 1;
228+
229+
switch (index) {
230+
0 => {
231+
// domain name, can start with #HttpOnly_
232+
if (std.mem.indexOf(u8, v, "#HttpOnly_")) |pos| {
233+
c.http_only = true;
234+
c.domain = try aa.dupe(u8, v[pos + "#HttpOnly_".len ..]);
235+
} else {
236+
c.http_only = false;
237+
c.domain = try aa.dupe(u8, v);
238+
}
239+
},
240+
1 => c.same_site = .lax, // TODO
241+
2 => c.path = try aa.dupe(u8, v),
242+
3 => c.secure = std.mem.eql(u8, "TRUE", v),
243+
4 => {
244+
if (std.mem.eql(u8, "0", v)) {
245+
c.expires = null;
246+
} else {
247+
const i = try std.fmt.parseInt(i64, v, 10);
248+
c.expires = @floatFromInt(i);
249+
}
250+
},
251+
5 => c.name = try aa.dupe(u8, v),
252+
6 => c.value = try aa.dupe(u8, v),
253+
else => return error.TooMuchCookieFields,
254+
}
255+
}
256+
257+
if (index != 7) return error.MissingCookieFields;
258+
259+
return c;
260+
}
261+
195262
// There's https://datatracker.ietf.org/doc/html/rfc6265 but browsers are
196263
// far less strict. I only found 2 cases where browsers will reject a cookie:
197264
// - a byte 0...32 and 127..255 anywhere in the cookie (the HTTP header
@@ -912,6 +979,25 @@ test "Cookie: parse domain" {
912979
try expectError(error.InvalidDomain, "http://lightpanda.io/", "b;domain=other.example.com");
913980
}
914981

982+
test "Cookie: parse curl" {
983+
try expectCookieCurl(.{
984+
.name = "cookie_key",
985+
.value = "cookie_value",
986+
.path = "/cookies/",
987+
.domain = "httpbin.io",
988+
.http_only = true,
989+
}, "#HttpOnly_httpbin.io\tFALSE\t/cookies/\tFALSE\t0\tcookie_key\tcookie_value");
990+
991+
try expectCookieCurl(.{
992+
.name = "cookie_key",
993+
.value = "cookie_value",
994+
.path = "/cookies/",
995+
.domain = "httpbin.io",
996+
.expires = 10,
997+
.secure = true,
998+
}, "httpbin.io\tTRUE\t/cookies/\tTRUE\t10\tcookie_key\tcookie_value");
999+
}
1000+
9151001
const ExpectedCookie = struct {
9161002
name: []const u8,
9171003
value: []const u8,
@@ -939,6 +1025,21 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
9391025
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
9401026
}
9411027

1028+
fn expectCookieCurl(expected: ExpectedCookie, set_cookie: []const u8) !void {
1029+
var cookie = try Cookie.parseCurl(testing.allocator, set_cookie);
1030+
defer cookie.deinit();
1031+
1032+
try testing.expectEqual(expected.name, cookie.name);
1033+
try testing.expectEqual(expected.value, cookie.value);
1034+
try testing.expectEqual(expected.secure, cookie.secure);
1035+
try testing.expectEqual(expected.http_only, cookie.http_only);
1036+
try testing.expectEqual(expected.same_site, cookie.same_site);
1037+
try testing.expectEqual(expected.path, cookie.path);
1038+
try testing.expectEqual(expected.domain, cookie.domain);
1039+
1040+
try testing.expectDelta(expected.expires, cookie.expires, 2.0);
1041+
}
1042+
9421043
fn expectAttribute(expected: anytype, url: ?[]const u8, set_cookie: []const u8) !void {
9431044
const uri = if (url) |u| try Uri.parse(u) else test_uri;
9441045
var cookie = try Cookie.parse(testing.allocator, &uri, set_cookie);

src/http/Client.zig

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,22 @@ pub const Transfer = struct {
588588
return 0;
589589
};
590590

591+
// Read all cookies.
592+
var cookies: ?*c.curl_slist = undefined;
593+
errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_COOKIELIST, &cookies)) catch |err| {
594+
log.err(.http, "failed to get cookies", .{ .err = err });
595+
return 0;
596+
};
597+
defer c.curl_slist_free_all(cookies);
598+
599+
// populate cookies in the jar
600+
while (cookies) |_cookie| {
601+
transfer.req.cookie_jar.populateFromCurl(std.mem.span(_cookie.data[0..])) catch |err| {
602+
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
603+
};
604+
cookies = _cookie.next;
605+
}
606+
591607
transfer.response_header = .{
592608
.url = url,
593609
.status = status,
@@ -609,18 +625,6 @@ pub const Transfer = struct {
609625
}
610626
}
611627

612-
{
613-
const SET_COOKIE_LEN = "set-cookie:".len;
614-
if (header.len > SET_COOKIE_LEN) {
615-
if (std.ascii.eqlIgnoreCase(header[0..SET_COOKIE_LEN], "set-cookie:")) {
616-
const value = std.mem.trimLeft(u8, header[SET_COOKIE_LEN..], " ");
617-
transfer.req.cookie_jar.populateFromResponse(&transfer.uri, value) catch |err| {
618-
log.err(.http, "set cookie", .{ .err = err, .req = transfer });
619-
};
620-
}
621-
}
622-
}
623-
624628
if (buf_len == 2) {
625629
transfer.req.header_done_callback(transfer) catch |err| {
626630
log.err(.http, "header_done_callback", .{ .err = err, .req = transfer });

src/http/Http.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub const Connection = struct {
107107
// redirect behavior
108108
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_MAXREDIRS, @as(c_long, @intCast(opts.max_redirects))));
109109
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 2)));
110+
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COOKIEFILE, "")); // enable curl's cookie engine
110111
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_REDIR_PROTOCOLS_STR, "HTTP,HTTPS")); // remove FTP and FTPS from the default
111112

112113
// proxy

0 commit comments

Comments
 (0)