diff --git a/src/network/cache/Cache.zig b/src/network/cache/Cache.zig index 159984f08..c336c406f 100644 --- a/src/network/cache/Cache.zig +++ b/src/network/cache/Cache.zig @@ -67,6 +67,19 @@ pub fn clear(self: *Cache) !void { }; } +/// RFC 9111 delta-seconds values larger than this are capped rather than +/// rejected (§1.2.2). Capping also keeps the value safely castable to i64 +/// for freshness arithmetic and storage. +const max_delta_seconds: u64 = 2147483648; + +fn parseDeltaSeconds(value: []const u8) ?u64 { + const seconds = std.fmt.parseInt(u64, value, 10) catch |err| switch (err) { + error.Overflow => return max_delta_seconds, + error.InvalidCharacter => return null, + }; + return @min(seconds, max_delta_seconds); +} + pub const CacheControl = struct { max_age: u64, must_revalidate: bool = false, @@ -79,16 +92,12 @@ pub const CacheControl = struct { var iter = std.mem.splitScalar(u8, value, ','); while (iter.next()) |part| { - const stripped = std.mem.trim(u8, part, &std.ascii.whitespace); + const directive = std.mem.trim(u8, part, &std.ascii.whitespace); - var buf: [16]u8 = undefined; - const len = @min(buf.len, stripped.len); - const directive = std.ascii.lowerString(buf[0..len], stripped[0..len]); - - if (std.mem.eql(u8, directive, "no-store")) { + if (std.ascii.eqlIgnoreCase(directive, "no-store")) { return null; } - if (std.mem.eql(u8, directive, "no-cache")) { + if (std.ascii.eqlIgnoreCase(directive, "no-cache")) { if (!max_age_set) { cc.max_age = 0; max_age_set = true; @@ -97,19 +106,19 @@ pub const CacheControl = struct { cc.must_revalidate = true; continue; } - if (std.mem.eql(u8, directive, "private")) { + if (std.ascii.eqlIgnoreCase(directive, "private")) { return null; } - if (std.mem.startsWith(u8, directive, "max-age=")) { + if (std.ascii.startsWithIgnoreCase(directive, "max-age=")) { if (!max_s_age_set) { - if (std.fmt.parseInt(u64, directive[8..], 10) catch null) |max_age| { + if (parseDeltaSeconds(directive[8..])) |max_age| { cc.max_age = max_age; max_age_set = true; } } - } else if (std.mem.startsWith(u8, directive, "s-maxage=")) { - if (std.fmt.parseInt(u64, directive[9..], 10) catch null) |max_age| { + } else if (std.ascii.startsWithIgnoreCase(directive, "s-maxage=")) { + if (parseDeltaSeconds(directive[9..])) |max_age| { cc.max_age = max_age; max_age_set = true; max_s_age_set = true; @@ -184,7 +193,7 @@ pub const CachedMetadata = struct { const value = h.value; if (std.ascii.eqlIgnoreCase("Age", name)) { - self.age_at_store = std.fmt.parseInt(u64, value, 10) catch 0; + self.age_at_store = parseDeltaSeconds(value) orelse 0; } else if (std.ascii.eqlIgnoreCase("Cache-Control", name)) { self.cache_control = CacheControl.parse(value) orelse continue; } else if (std.ascii.eqlIgnoreCase("ETag", name)) { @@ -300,7 +309,7 @@ pub fn tryCache( .content_type = if (content_type) |ct| try arena.dupe(u8, ct) else "application/octet-stream", .status = status, .stored_at = timestamp, - .age_at_store = if (age) |a| std.fmt.parseInt(u64, a, 10) catch 0 else 0, + .age_at_store = if (age) |a| parseDeltaSeconds(a) orelse 0 else 0, .cache_control = cc, .headers = &.{}, .vary_headers = &.{}, @@ -342,6 +351,15 @@ test "Cache: CacheControl.parse" { try testing.expectEqual(null, CacheControl.parse("max-age=abc")); try testing.expectEqual(null, CacheControl.parse("max-age=")); + + // values longer than 8 digits must not be truncated + try testing.expectEqual(315360000, CacheControl.parse("max-age=315360000").?.max_age); + + // delta-seconds too large to represent are capped at 2^31 (RFC 9111 §1.2.2) + try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=2147483649").?.max_age); + try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=9999999999999999999").?.max_age); + try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=99999999999999999999999").?.max_age); + try testing.expectEqual(max_delta_seconds, CacheControl.parse("s-maxage=9999999999999999999").?.max_age); } test "Cache: CachedMetadata.renew updates timestamp and age" { @@ -381,6 +399,14 @@ test "Cache: CachedMetadata.renew updates age from Age header" { }); try testing.expectEqual(42, meta.age_at_store); + + meta.renew(.{ + .url = "https://example.com", + .timestamp = 2000, + .headers = &.{.{ .name = "Age", .value = "18446744073709551615" }}, + }); + + try testing.expectEqual(max_delta_seconds, meta.age_at_store); } test "Cache: CachedMetadata.renew updates cache_control" {