mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
332 lines
12 KiB
Zig
332 lines
12 KiB
Zig
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
|
//
|
|
// Francis Bouvier <francis@lightpanda.io>
|
|
// Pierre Tachoire <pierre@lightpanda.io>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
const std = @import("std");
|
|
const lp = @import("lightpanda");
|
|
|
|
const Layer = @import("../../browser/HttpClient.zig").Layer;
|
|
const Request = @import("../../browser/HttpClient.zig").Request;
|
|
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
|
|
const Response = @import("../../browser/HttpClient.zig").Response;
|
|
|
|
const Cache = @import("../cache/Cache.zig");
|
|
const CachedMetadata = @import("../cache/Cache.zig").CachedMetadata;
|
|
const CachedResponse = @import("../cache/Cache.zig").CachedResponse;
|
|
|
|
const Forward = @import("Forward.zig");
|
|
|
|
const log = lp.log;
|
|
const IS_DEBUG = @import("builtin").mode == .Debug;
|
|
|
|
const CacheLayer = @This();
|
|
|
|
next: Layer = undefined,
|
|
disabled: bool = false,
|
|
|
|
pub fn layer(self: *CacheLayer) Layer {
|
|
return .{
|
|
.ptr = self,
|
|
.vtable = &.{
|
|
.request = request,
|
|
},
|
|
};
|
|
}
|
|
|
|
fn request(ptr: *anyopaque, transfer: *Transfer) anyerror!void {
|
|
const self: *CacheLayer = @ptrCast(@alignCast(ptr));
|
|
const req = &transfer.req;
|
|
|
|
if (self.disabled or req.method != .GET) {
|
|
return self.next.request(transfer);
|
|
}
|
|
|
|
const arena = transfer.arena;
|
|
|
|
var iter = req.headers.iterator();
|
|
const req_header_list = try iter.collect(arena);
|
|
|
|
if (transfer.client.network.cache.?.get(arena, .{
|
|
.url = req.url,
|
|
.timestamp = std.time.timestamp(),
|
|
.request_headers = req_header_list.items,
|
|
})) |cached| {
|
|
if (cached.expired) {
|
|
if (cached.metadata.hasValidators()) {
|
|
if (cached.metadata.etag) |etag| {
|
|
log.debug(.cache, "revalidate with etag", .{ .url = req.url, .etag = etag });
|
|
const header_value = try std.fmt.allocPrintSentinel(arena, "If-None-Match: {s}", .{etag}, 0);
|
|
try req.headers.add(header_value);
|
|
} else if (cached.metadata.last_modified) |lm| {
|
|
log.debug(.cache, "revalidate with last-modified", .{ .url = req.url, .last_modified = lm });
|
|
const header_value = try std.fmt.allocPrintSentinel(arena, "If-Modified-Since: {s}", .{lm}, 0);
|
|
try req.headers.add(header_value);
|
|
}
|
|
|
|
try installCacheContext(arena, transfer, cached);
|
|
return self.next.request(transfer);
|
|
} else {
|
|
// If it is expired w/o validators, evict from Cache.
|
|
transfer.client.network.cache.?.evict(req.url);
|
|
}
|
|
} else {
|
|
// Dispatch that the Request was served from the Cache.
|
|
transfer.req.notification.dispatch(
|
|
.http_request_served_from_cache,
|
|
&.{ .transfer = transfer },
|
|
);
|
|
|
|
const ctx = try arena.create(CachedResponse);
|
|
ctx.* = cached;
|
|
|
|
try transfer.client.runNextTick(transfer, ctx, .{
|
|
.run = struct {
|
|
fn run(t: *Transfer, ctx_ptr: ?*anyopaque) void {
|
|
defer t.deinit();
|
|
|
|
const c: *CachedResponse = @ptrCast(@alignCast(ctx_ptr.?));
|
|
serveFromCache(&t.req, c) catch |err| {
|
|
t.req.error_callback(t.req.ctx, err);
|
|
};
|
|
}
|
|
}.run,
|
|
.abort = struct {
|
|
fn abort(ctx_ptr: ?*anyopaque) void {
|
|
const c: *CachedResponse = @ptrCast(@alignCast(ctx_ptr.?));
|
|
switch (c.data) {
|
|
.buffer => |_| {},
|
|
.file => |f| f.file.close(),
|
|
}
|
|
}
|
|
}.abort,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Cache miss: install wrappers so we can inspect the response and decide
|
|
// whether to write the body into the cache when it's done.
|
|
try installCacheContext(arena, transfer, null);
|
|
return self.next.request(transfer);
|
|
}
|
|
|
|
fn installCacheContext(
|
|
arena: std.mem.Allocator,
|
|
transfer: *Transfer,
|
|
stale_entry: ?CachedResponse,
|
|
) !void {
|
|
const req = &transfer.req;
|
|
const ctx = try arena.create(CacheContext);
|
|
ctx.* = .{
|
|
.arena = arena,
|
|
.transfer = transfer,
|
|
.forward = Forward.capture(req),
|
|
.req_url = req.url,
|
|
.req_headers = req.headers,
|
|
.stale_entry = stale_entry,
|
|
};
|
|
|
|
req.ctx = ctx;
|
|
req.header_callback = CacheContext.headerCallback;
|
|
req.data_callback = CacheContext.dataCallback;
|
|
req.done_callback = CacheContext.doneCallback;
|
|
req.error_callback = CacheContext.errorCallback;
|
|
|
|
if (ctx.forward.start != null) req.start_callback = CacheContext.startCallback;
|
|
if (ctx.forward.shutdown != null) req.shutdown_callback = CacheContext.shutdownCallback;
|
|
}
|
|
|
|
fn serveFromCache(req: *Request, cached: *const CachedResponse) !void {
|
|
const response = Response.fromCached(req.ctx, cached);
|
|
defer cached.data.deinit();
|
|
|
|
if (req.start_callback) |cb| {
|
|
try cb(response);
|
|
}
|
|
|
|
const proceed = try req.header_callback(response);
|
|
if (!proceed) {
|
|
return error.Abort;
|
|
}
|
|
|
|
switch (cached.data) {
|
|
.buffer => |data| {
|
|
if (data.len > 0) {
|
|
try req.data_callback(response, data);
|
|
}
|
|
},
|
|
.file => |f| {
|
|
const file = f.file;
|
|
var buf: [1024]u8 = undefined;
|
|
var file_reader = file.reader(&buf);
|
|
try file_reader.seekTo(f.offset);
|
|
const reader = &file_reader.interface;
|
|
var read_buf: [1024]u8 = undefined;
|
|
var remaining = f.len;
|
|
while (remaining > 0) {
|
|
const read_len = @min(read_buf.len, remaining);
|
|
const n = try reader.readSliceShort(read_buf[0..read_len]);
|
|
if (n == 0) break;
|
|
remaining -= n;
|
|
try req.data_callback(response, read_buf[0..n]);
|
|
}
|
|
},
|
|
}
|
|
|
|
try req.done_callback(req.ctx);
|
|
}
|
|
|
|
const CacheContext = struct {
|
|
arena: std.mem.Allocator,
|
|
transfer: *Transfer,
|
|
forward: Forward,
|
|
req_url: [:0]const u8,
|
|
req_headers: @import("../http.zig").Headers,
|
|
pending_metadata: ?*CachedMetadata = null,
|
|
stale_entry: ?CachedResponse = null,
|
|
|
|
fn startCallback(response: Response) anyerror!void {
|
|
const self: *CacheContext = @ptrCast(@alignCast(response.ctx));
|
|
return self.forward.forwardStart(response);
|
|
}
|
|
|
|
fn dataCallback(response: Response, chunk: []const u8) anyerror!void {
|
|
const self: *CacheContext = @ptrCast(@alignCast(response.ctx));
|
|
return self.forward.forwardData(response, chunk);
|
|
}
|
|
|
|
fn headerCallback(response: Response) anyerror!bool {
|
|
const self: *CacheContext = @ptrCast(@alignCast(response.ctx));
|
|
defer {
|
|
if (self.stale_entry) |stale| {
|
|
stale.data.deinit();
|
|
}
|
|
self.stale_entry = null;
|
|
}
|
|
|
|
// For non-transfer responses (fulfilled by interception, or future
|
|
// cached-while-cached cases), there's nothing to inspect for caching
|
|
// decisions — just forward.
|
|
const transfer = switch (response.inner) {
|
|
.transfer => |t| t,
|
|
else => return self.forward.forwardHeader(response),
|
|
};
|
|
|
|
const arena = self.arena;
|
|
const conn = transfer._conn.?;
|
|
var rh = &transfer.res.header.?;
|
|
|
|
if (self.stale_entry != null and rh.status == 304) {
|
|
const stale = self.stale_entry.?;
|
|
self.stale_entry = null;
|
|
|
|
transfer.client.network.cache.?.revalidate(
|
|
arena,
|
|
self.req_url,
|
|
std.time.timestamp(),
|
|
) catch |err| {
|
|
log.warn(.cache, "revalidate failed", .{ .err = err });
|
|
};
|
|
|
|
transfer.req.notification.dispatch(
|
|
.http_request_served_from_cache,
|
|
&.{ .transfer = transfer },
|
|
);
|
|
|
|
serveFromCache(&transfer.req, &stale) catch |err| {
|
|
self.forward.forwardErr(err);
|
|
};
|
|
|
|
return false;
|
|
}
|
|
|
|
const vary = if (conn.getResponseHeader("vary", 0)) |h| h.value else null;
|
|
const maybe_cm = try Cache.tryCache(
|
|
arena,
|
|
std.time.timestamp(),
|
|
self.req_url,
|
|
rh.status,
|
|
rh.contentType(),
|
|
if (conn.getResponseHeader("cache-control", 0)) |h| h.value else null,
|
|
vary,
|
|
if (conn.getResponseHeader("age", 0)) |h| h.value else null,
|
|
if (conn.getResponseHeader("etag", 0)) |h| h.value else null,
|
|
if (conn.getResponseHeader("last-modified", 0)) |h| h.value else null,
|
|
conn.getResponseHeader("set-cookie", 0) != null,
|
|
conn.getResponseHeader("authorization", 0) != null,
|
|
);
|
|
|
|
if (maybe_cm) |cm| {
|
|
var iter = transfer.responseHeaderIterator();
|
|
var header_list = try iter.collect(arena);
|
|
const end_of_response = header_list.items.len;
|
|
|
|
if (vary) |vary_str| {
|
|
var req_it = self.req_headers.iterator();
|
|
while (req_it.next()) |hdr| {
|
|
var vary_iter = std.mem.splitScalar(u8, vary_str, ',');
|
|
while (vary_iter.next()) |part| {
|
|
const name = std.mem.trim(u8, part, &std.ascii.whitespace);
|
|
if (std.ascii.eqlIgnoreCase(hdr.name, name)) {
|
|
try header_list.append(arena, .{
|
|
.name = try arena.dupe(u8, hdr.name),
|
|
.value = try arena.dupe(u8, hdr.value),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const metadata = try arena.create(CachedMetadata);
|
|
metadata.* = cm;
|
|
metadata.headers = header_list.items[0..end_of_response];
|
|
metadata.vary_headers = header_list.items[end_of_response..];
|
|
self.pending_metadata = metadata;
|
|
}
|
|
|
|
return self.forward.forwardHeader(response);
|
|
}
|
|
|
|
fn doneCallback(ctx: *anyopaque) anyerror!void {
|
|
const self: *CacheContext = @ptrCast(@alignCast(ctx));
|
|
const transfer = self.transfer;
|
|
|
|
if (self.pending_metadata) |metadata| {
|
|
const cache = &transfer.client.network.cache.?;
|
|
|
|
if (comptime IS_DEBUG) {
|
|
log.debug(.browser, "http cache", .{ .key = self.req_url, .metadata = metadata });
|
|
}
|
|
cache.put(metadata.*, transfer.res.stream_buffer.items) catch |err| {
|
|
log.warn(.http, "cache put failed", .{ .err = err });
|
|
};
|
|
}
|
|
|
|
return self.forward.forwardDone();
|
|
}
|
|
|
|
fn shutdownCallback(ctx: *anyopaque) void {
|
|
const self: *CacheContext = @ptrCast(@alignCast(ctx));
|
|
self.forward.forwardShutdown();
|
|
}
|
|
|
|
fn errorCallback(ctx: *anyopaque, e: anyerror) void {
|
|
const self: *CacheContext = @ptrCast(@alignCast(ctx));
|
|
self.forward.forwardErr(e);
|
|
}
|
|
};
|