mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-04 00:20:32 +00:00
try layering http client
This commit is contained in:
@@ -260,14 +260,14 @@ pub const Client = struct {
|
|||||||
|
|
||||||
fn start(self: *Client) void {
|
fn start(self: *Client) void {
|
||||||
const http = self.http;
|
const http = self.http;
|
||||||
http.cdp_client = .{
|
http.setCdpClient(.{
|
||||||
.socket = self.ws.socket,
|
.socket = self.ws.socket,
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.blocking_read_start = Client.blockingReadStart,
|
.blocking_read_start = Client.blockingReadStart,
|
||||||
.blocking_read = Client.blockingRead,
|
.blocking_read = Client.blockingRead,
|
||||||
.blocking_read_end = Client.blockingReadStop,
|
.blocking_read_end = Client.blockingReadStop,
|
||||||
};
|
});
|
||||||
defer http.cdp_client = null;
|
defer http.setCdpClient(null);
|
||||||
|
|
||||||
self.httpLoop(http) catch |err| {
|
self.httpLoop(http) catch |err| {
|
||||||
log.err(.app, "CDP client loop", .{ .err = err });
|
log.err(.app, "CDP client loop", .{ .err = err });
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -135,7 +135,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
|||||||
.pre, .raw, .text, .image => {
|
.pre, .raw, .text, .image => {
|
||||||
// The main page hasn't started/finished navigating.
|
// The main page hasn't started/finished navigating.
|
||||||
// There's no JS to run, and no reason to run the scheduler.
|
// There's no JS to run, and no reason to run the scheduler.
|
||||||
if (http_client.active == 0 and (comptime is_cdp) == false) {
|
if (http_client.active() == 0 and (comptime is_cdp) == false) {
|
||||||
// haven't started navigating, I guess.
|
// haven't started navigating, I guess.
|
||||||
return .done;
|
return .done;
|
||||||
}
|
}
|
||||||
@@ -169,8 +169,8 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
|||||||
// Each call to this runs scheduled load events.
|
// Each call to this runs scheduled load events.
|
||||||
try page.dispatchLoad();
|
try page.dispatchLoad();
|
||||||
|
|
||||||
const http_active = http_client.active;
|
const http_active = http_client.active();
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
const total_network_activity = http_active + http_client.intercepted();
|
||||||
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
page.notifyNetworkAlmostIdle();
|
page.notifyNetworkAlmostIdle();
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult {
|
|||||||
// because is_cdp is true, and that can only be
|
// because is_cdp is true, and that can only be
|
||||||
// the case when interception isn't possible.
|
// the case when interception isn't possible.
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
std.debug.assert(http_client.intercepted == 0);
|
std.debug.assert(http_client.intercepted() == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (browser.hasBackgroundTasks()) {
|
if (browser.hasBackgroundTasks()) {
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void {
|
|||||||
try sendPageLifecycle(bc, "load", now, frame_id, loader_id);
|
try sendPageLifecycle(bc, "load", now, frame_id, loader_id);
|
||||||
|
|
||||||
const http_client = page._session.browser.http_client;
|
const http_client = page._session.browser.http_client;
|
||||||
const http_active = http_client.active;
|
const http_active = http_client.active();
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
const total_network_activity = http_active + http_client.intercepted();
|
||||||
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
try sendPageLifecycle(bc, "networkAlmostIdle", now, frame_id, loader_id);
|
try sendPageLifecycle(bc, "networkAlmostIdle", now, frame_id, loader_id);
|
||||||
}
|
}
|
||||||
|
|||||||
240
src/network/layer/CacheLayer.zig
Normal file
240
src/network/layer/CacheLayer.zig
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// 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 log = @import("../../log.zig");
|
||||||
|
|
||||||
|
const http = @import("../http.zig");
|
||||||
|
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
|
||||||
|
const Context = @import("../../browser/HttpClient.zig").Context;
|
||||||
|
const Request = @import("../../browser/HttpClient.zig").Request;
|
||||||
|
const Response = @import("../../browser/HttpClient.zig").Response;
|
||||||
|
const Layer = @import("../../browser/HttpClient.zig").Layer;
|
||||||
|
|
||||||
|
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 CacheLayer = @This();
|
||||||
|
|
||||||
|
next: Layer = undefined,
|
||||||
|
|
||||||
|
pub fn layer(self: *CacheLayer) Layer {
|
||||||
|
return .{
|
||||||
|
.ptr = self,
|
||||||
|
.vtable = &.{
|
||||||
|
.request = request,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(ptr: *anyopaque, ctx: Context, req: Request) anyerror!void {
|
||||||
|
const self: *CacheLayer = @ptrCast(@alignCast(ptr));
|
||||||
|
const network = ctx.network;
|
||||||
|
|
||||||
|
if (network.cache == null or req.method != .GET) {
|
||||||
|
return self.next.request(ctx, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arena = try network.app.arena_pool.acquire(.{ .debug = "CacheLayer" });
|
||||||
|
errdefer network.app.arena_pool.release(arena);
|
||||||
|
|
||||||
|
var iter = req.headers.iterator();
|
||||||
|
const req_header_list = try iter.collect(arena);
|
||||||
|
|
||||||
|
if (network.cache.?.get(arena, .{
|
||||||
|
.url = req.url,
|
||||||
|
.timestamp = std.time.timestamp(),
|
||||||
|
.request_headers = req_header_list.items,
|
||||||
|
})) |cached| {
|
||||||
|
defer req.headers.deinit();
|
||||||
|
defer network.app.arena_pool.release(arena);
|
||||||
|
return serveFromCache(req, &cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cache_ctx = try arena.create(CacheContext);
|
||||||
|
cache_ctx.* = .{
|
||||||
|
.arena = arena,
|
||||||
|
.context = ctx,
|
||||||
|
.forward = Forward.fromRequest(req),
|
||||||
|
.req_url = req.url,
|
||||||
|
.req_headers = req.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapped = cache_ctx.forward.wrapRequest(
|
||||||
|
req,
|
||||||
|
cache_ctx,
|
||||||
|
"forward",
|
||||||
|
.{
|
||||||
|
.start = CacheContext.startCallback,
|
||||||
|
.header = CacheContext.headerCallback,
|
||||||
|
.done = CacheContext.doneCallback,
|
||||||
|
.shutdown = CacheContext.shutdownCallback,
|
||||||
|
.err = CacheContext.errorCallback,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return self.next.request(ctx, wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serveFromCache(req: Request, cached: *const CachedResponse) !void {
|
||||||
|
const response = Response.fromCached(req.ctx, cached);
|
||||||
|
defer switch (cached.data) {
|
||||||
|
.buffer => |_| {},
|
||||||
|
.file => |f| f.file.close(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (req.start_callback) |cb| {
|
||||||
|
try cb(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proceed = try req.header_callback(response);
|
||||||
|
if (!proceed) {
|
||||||
|
req.error_callback(req.ctx, error.Abort);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
context: Context,
|
||||||
|
transfer: ?*Transfer = null,
|
||||||
|
forward: Forward,
|
||||||
|
req_url: [:0]const u8,
|
||||||
|
req_headers: http.Headers,
|
||||||
|
pending_metadata: ?*CachedMetadata = null,
|
||||||
|
|
||||||
|
fn startCallback(response: Response) anyerror!void {
|
||||||
|
const self: *CacheContext = @ptrCast(@alignCast(response.ctx));
|
||||||
|
self.transfer = response.inner.transfer;
|
||||||
|
return self.forward.forwardStart(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn headerCallback(response: Response) anyerror!bool {
|
||||||
|
const self: *CacheContext = @ptrCast(@alignCast(response.ctx));
|
||||||
|
const allocator = self.arena;
|
||||||
|
|
||||||
|
const transfer = response.inner.transfer;
|
||||||
|
var rh = &transfer.response_header.?;
|
||||||
|
|
||||||
|
const conn = transfer._conn.?;
|
||||||
|
|
||||||
|
const vary = if (conn.getResponseHeader("vary", 0)) |h| h.value else null;
|
||||||
|
|
||||||
|
const maybe_cm = try Cache.tryCache(
|
||||||
|
allocator,
|
||||||
|
std.time.timestamp(),
|
||||||
|
transfer.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,
|
||||||
|
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(allocator);
|
||||||
|
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(allocator, .{
|
||||||
|
.name = try allocator.dupe(u8, hdr.name),
|
||||||
|
.value = try allocator.dupe(u8, hdr.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = try allocator.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));
|
||||||
|
defer self.context.network.app.arena_pool.release(self.arena);
|
||||||
|
|
||||||
|
const transfer = self.transfer orelse @panic("Start Callback didn't set CacheLayer.transfer");
|
||||||
|
|
||||||
|
if (self.pending_metadata) |metadata| {
|
||||||
|
const cache = &self.context.network.cache.?;
|
||||||
|
|
||||||
|
log.debug(.browser, "http cache", .{ .key = self.req_url, .metadata = metadata });
|
||||||
|
cache.put(metadata.*, transfer._stream_buffer.items) catch |err| {
|
||||||
|
log.warn(.http, "cache put failed", .{ .err = err });
|
||||||
|
};
|
||||||
|
log.debug(.browser, "http.cache.put", .{ .url = self.req_url });
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.forward.forwardDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shutdownCallback(ctx: *anyopaque) void {
|
||||||
|
const self: *CacheContext = @ptrCast(@alignCast(ctx));
|
||||||
|
defer self.context.network.app.arena_pool.release(self.arena);
|
||||||
|
self.forward.forwardShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn errorCallback(ctx: *anyopaque, e: anyerror) void {
|
||||||
|
const self: *CacheContext = @ptrCast(@alignCast(ctx));
|
||||||
|
defer self.context.network.app.arena_pool.release(self.arena);
|
||||||
|
self.forward.forwardErr(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
135
src/network/layer/Forward.zig
Normal file
135
src/network/layer/Forward.zig
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// 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 Request = @import("../../browser/HttpClient.zig").Request;
|
||||||
|
const Response = @import("../../browser/HttpClient.zig").Response;
|
||||||
|
|
||||||
|
const Forward = @This();
|
||||||
|
|
||||||
|
ctx: *anyopaque,
|
||||||
|
start: ?Request.StartCallback,
|
||||||
|
header: Request.HeaderCallback,
|
||||||
|
data: Request.DataCallback,
|
||||||
|
done: Request.DoneCallback,
|
||||||
|
err: Request.ErrorCallback,
|
||||||
|
shutdown: ?Request.ShutdownCallback,
|
||||||
|
|
||||||
|
pub fn fromRequest(req: Request) Forward {
|
||||||
|
return .{
|
||||||
|
.ctx = req.ctx,
|
||||||
|
.start = req.start_callback,
|
||||||
|
.header = req.header_callback,
|
||||||
|
.data = req.data_callback,
|
||||||
|
.done = req.done_callback,
|
||||||
|
.err = req.error_callback,
|
||||||
|
.shutdown = req.shutdown_callback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Overrides = struct {
|
||||||
|
start: ?Request.StartCallback = null,
|
||||||
|
header: ?Request.HeaderCallback = null,
|
||||||
|
data: ?Request.DataCallback = null,
|
||||||
|
done: ?Request.DoneCallback = null,
|
||||||
|
err: ?Request.ErrorCallback = null,
|
||||||
|
shutdown: ?Request.ShutdownCallback = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn wrapRequest(
|
||||||
|
self: *Forward,
|
||||||
|
req: Request,
|
||||||
|
new_ctx: anytype,
|
||||||
|
comptime field: []const u8,
|
||||||
|
overrides: Overrides,
|
||||||
|
) Request {
|
||||||
|
const T = @TypeOf(new_ctx.*);
|
||||||
|
const PassthroughT = makePassthrough(T, field);
|
||||||
|
var wrapped = req;
|
||||||
|
wrapped.ctx = new_ctx;
|
||||||
|
wrapped.start_callback = overrides.start orelse if (self.start != null) PassthroughT.start else null;
|
||||||
|
wrapped.header_callback = overrides.header orelse PassthroughT.header;
|
||||||
|
wrapped.data_callback = overrides.data orelse PassthroughT.data;
|
||||||
|
wrapped.done_callback = overrides.done orelse PassthroughT.done;
|
||||||
|
wrapped.error_callback = overrides.err orelse PassthroughT.err;
|
||||||
|
wrapped.shutdown_callback = overrides.shutdown orelse if (self.shutdown != null) PassthroughT.shutdown else null;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn makePassthrough(comptime T: type, comptime field: []const u8) type {
|
||||||
|
return struct {
|
||||||
|
pub fn start(response: Response) anyerror!void {
|
||||||
|
const self: *T = @ptrCast(@alignCast(response.ctx));
|
||||||
|
return @field(self, field).forwardStart(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(response: Response) anyerror!bool {
|
||||||
|
const self: *T = @ptrCast(@alignCast(response.ctx));
|
||||||
|
return @field(self, field).forwardHeader(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data(response: Response, chunk: []const u8) anyerror!void {
|
||||||
|
const self: *T = @ptrCast(@alignCast(response.ctx));
|
||||||
|
return @field(self, field).forwardData(response, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn done(ctx_ptr: *anyopaque) anyerror!void {
|
||||||
|
const self: *T = @ptrCast(@alignCast(ctx_ptr));
|
||||||
|
return @field(self, field).forwardDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn err(ctx_ptr: *anyopaque, e: anyerror) void {
|
||||||
|
const self: *T = @ptrCast(@alignCast(ctx_ptr));
|
||||||
|
@field(self, field).forwardErr(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(ctx_ptr: *anyopaque) void {
|
||||||
|
const self: *T = @ptrCast(@alignCast(ctx_ptr));
|
||||||
|
@field(self, field).forwardShutdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardStart(self: Forward, response: Response) anyerror!void {
|
||||||
|
var fwd = response;
|
||||||
|
fwd.ctx = self.ctx;
|
||||||
|
if (self.start) |cb| try cb(fwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardHeader(self: Forward, response: Response) anyerror!bool {
|
||||||
|
var fwd = response;
|
||||||
|
fwd.ctx = self.ctx;
|
||||||
|
return self.header(fwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardData(self: Forward, response: Response, chunk: []const u8) anyerror!void {
|
||||||
|
var fwd = response;
|
||||||
|
fwd.ctx = self.ctx;
|
||||||
|
return self.data(fwd, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardDone(self: Forward) anyerror!void {
|
||||||
|
return self.done(self.ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardErr(self: Forward, e: anyerror) void {
|
||||||
|
self.err(self.ctx, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forwardShutdown(self: Forward) void {
|
||||||
|
if (self.shutdown) |cb| cb(self.ctx);
|
||||||
|
}
|
||||||
0
src/network/layer/RobotsLayer.zig
Normal file
0
src/network/layer/RobotsLayer.zig
Normal file
0
src/network/layer/WebBotAuthLayer.zig
Normal file
0
src/network/layer/WebBotAuthLayer.zig
Normal file
Reference in New Issue
Block a user