mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
3# This is a combination of 3 commits.
intercept continue and abort feedback First version of headers, no cookies yet
This commit is contained in:
@@ -57,7 +57,6 @@ deferreds: OrderList,
|
|||||||
|
|
||||||
shutdown: bool = false,
|
shutdown: bool = false,
|
||||||
|
|
||||||
|
|
||||||
client: *HttpClient,
|
client: *HttpClient,
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
buffer_pool: BufferPool,
|
buffer_pool: BufferPool,
|
||||||
@@ -234,6 +233,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
|
|||||||
.url = remote_url.?,
|
.url = remote_url.?,
|
||||||
.ctx = pending_script,
|
.ctx = pending_script,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
.headers = try HttpClient.Headers.init(),
|
||||||
.cookie = page.requestCookie(.{}),
|
.cookie = page.requestCookie(.{}),
|
||||||
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) startCallback else null,
|
||||||
.header_done_callback = headerCallback,
|
.header_done_callback = headerCallback,
|
||||||
@@ -297,6 +297,7 @@ pub fn blockingGet(self: *ScriptManager, url: [:0]const u8) !BlockingResult {
|
|||||||
try client.blockingRequest(.{
|
try client.blockingRequest(.{
|
||||||
.url = url,
|
.url = url,
|
||||||
.method = .GET,
|
.method = .GET,
|
||||||
|
.headers = try HttpClient.Headers.init(),
|
||||||
.ctx = &blocking,
|
.ctx = &blocking,
|
||||||
.cookie = self.page.requestCookie(.{}),
|
.cookie = self.page.requestCookie(.{}),
|
||||||
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
.start_callback = if (log.enabled(.http, .debug)) Blocking.startCallback else null,
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ pub const Browser = struct {
|
|||||||
errdefer env.deinit();
|
errdefer env.deinit();
|
||||||
|
|
||||||
const notification = try Notification.init(allocator, app.notification);
|
const notification = try Notification.init(allocator, app.notification);
|
||||||
|
app.http.client.notification = notification;
|
||||||
|
app.http.client.next_request_id = 0; // Should we track ids in CDP only?
|
||||||
errdefer notification.deinit();
|
errdefer notification.deinit();
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -74,6 +76,7 @@ pub const Browser = struct {
|
|||||||
self.page_arena.deinit();
|
self.page_arena.deinit();
|
||||||
self.session_arena.deinit();
|
self.session_arena.deinit();
|
||||||
self.transfer_arena.deinit();
|
self.transfer_arena.deinit();
|
||||||
|
self.http_client.notification = null;
|
||||||
self.notification.deinit();
|
self.notification.deinit();
|
||||||
self.state_pool.deinit();
|
self.state_pool.deinit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -467,12 +467,15 @@ pub const Page = struct {
|
|||||||
const owned_url = try self.arena.dupeZ(u8, request_url);
|
const owned_url = try self.arena.dupeZ(u8, request_url);
|
||||||
self.url = try URL.parse(owned_url, null);
|
self.url = try URL.parse(owned_url, null);
|
||||||
|
|
||||||
|
var headers = try HttpClient.Headers.init();
|
||||||
|
if (opts.header) |hdr| try headers.add(hdr);
|
||||||
|
|
||||||
self.http_client.request(.{
|
self.http_client.request(.{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.url = owned_url,
|
.url = owned_url,
|
||||||
.method = opts.method,
|
.method = opts.method,
|
||||||
|
.headers = headers,
|
||||||
.body = opts.body,
|
.body = opts.body,
|
||||||
.header = opts.header,
|
|
||||||
.cookie = self.requestCookie(.{ .is_navigation = true }),
|
.cookie = self.requestCookie(.{ .is_navigation = true }),
|
||||||
.header_done_callback = pageHeaderDoneCallback,
|
.header_done_callback = pageHeaderDoneCallback,
|
||||||
.data_callback = pageDataCallback,
|
.data_callback = pageDataCallback,
|
||||||
|
|||||||
@@ -370,10 +370,16 @@ pub const XMLHttpRequest = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var headers = try HttpClient.Headers.init();
|
||||||
|
for (self.headers.items) |hdr| {
|
||||||
|
try headers.add(hdr);
|
||||||
|
}
|
||||||
|
|
||||||
try page.http_client.request(.{
|
try page.http_client.request(.{
|
||||||
.ctx = self,
|
.ctx = self,
|
||||||
.url = self.url.?,
|
.url = self.url.?,
|
||||||
.method = self.method,
|
.method = self.method,
|
||||||
|
.headers = headers,
|
||||||
.body = self.request_body,
|
.body = self.request_body,
|
||||||
.cookie = page.requestCookie(.{}),
|
.cookie = page.requestCookie(.{}),
|
||||||
.start_callback = httpStartCallback,
|
.start_callback = httpStartCallback,
|
||||||
@@ -387,11 +393,6 @@ pub const XMLHttpRequest = struct {
|
|||||||
|
|
||||||
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
|
||||||
const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
|
const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
|
||||||
|
|
||||||
for (self.headers.items) |hdr| {
|
|
||||||
try transfer.addHeader(hdr);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "xhr" });
|
log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "xhr" });
|
||||||
self.transfer = transfer;
|
self.transfer = transfer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const Page = @import("../browser/page.zig").Page;
|
|||||||
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
const Inspector = @import("../browser/env.zig").Env.Inspector;
|
||||||
const Incrementing = @import("../id.zig").Incrementing;
|
const Incrementing = @import("../id.zig").Incrementing;
|
||||||
const Notification = @import("../notification.zig").Notification;
|
const Notification = @import("../notification.zig").Notification;
|
||||||
|
const InterceptState = @import("domains/fetch.zig").InterceptState;
|
||||||
|
|
||||||
const polyfill = @import("../browser/polyfill/polyfill.zig");
|
const polyfill = @import("../browser/polyfill/polyfill.zig");
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
// Extra headers to add to all requests. TBD under which conditions this should be reset.
|
// Extra headers to add to all requests. TBD under which conditions this should be reset.
|
||||||
extra_headers: std.ArrayListUnmanaged(std.http.Header) = .empty,
|
extra_headers: std.ArrayListUnmanaged(std.http.Header) = .empty,
|
||||||
|
|
||||||
|
intercept_state: InterceptState,
|
||||||
|
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
pub fn init(app: *App, client: TypeProvider.Client) !Self {
|
pub fn init(app: *App, client: TypeProvider.Client) !Self {
|
||||||
@@ -89,6 +92,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
.browser_context = null,
|
.browser_context = null,
|
||||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
.notification_arena = std.heap.ArenaAllocator.init(allocator),
|
.notification_arena = std.heap.ArenaAllocator.init(allocator),
|
||||||
|
.intercept_state = try InterceptState.init(allocator), // TBD or browser session arena?
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +100,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
|||||||
if (self.browser_context) |*bc| {
|
if (self.browser_context) |*bc| {
|
||||||
bc.deinit();
|
bc.deinit();
|
||||||
}
|
}
|
||||||
|
self.intercept_state.deinit(); // TBD Should this live in BC?
|
||||||
self.browser.deinit();
|
self.browser.deinit();
|
||||||
self.message_arena.deinit();
|
self.message_arena.deinit();
|
||||||
self.notification_arena.deinit();
|
self.notification_arena.deinit();
|
||||||
@@ -451,6 +456,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
self.cdp.browser.notification.unregister(.http_request_complete, self);
|
self.cdp.browser.notification.unregister(.http_request_complete, self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fetchEnable(self: *Self) !void {
|
||||||
|
try self.cdp.browser.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetchDisable(self: *Self) void {
|
||||||
|
self.cdp.browser.notification.unregister(.http_request_intercept, self);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
|
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
|
||||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||||
return @import("domains/page.zig").pageRemove(self);
|
return @import("domains/page.zig").pageRemove(self);
|
||||||
@@ -475,7 +488,13 @@ pub fn BrowserContext(comptime CDP_T: type) type {
|
|||||||
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
|
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
|
||||||
const self: *Self = @alignCast(@ptrCast(ctx));
|
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||||
defer self.resetNotificationArena();
|
defer self.resetNotificationArena();
|
||||||
return @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
|
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn onHttpRequestIntercept(ctx: *anyopaque, data: *const Notification.RequestIntercept) !void {
|
||||||
|
const self: *Self = @alignCast(@ptrCast(ctx));
|
||||||
|
defer self.resetNotificationArena();
|
||||||
|
try @import("domains/fetch.zig").requestPaused(self.notification_arena, self, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
|
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
//
|
//
|
||||||
// Francis Bouvier <francis@lightpanda.io>
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
// Pierre Tachoire <pierre@lightpanda.io>
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
@@ -17,13 +17,211 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Notification = @import("../../notification.zig").Notification;
|
||||||
|
const log = @import("../../log.zig");
|
||||||
|
const Request = @import("../../http/Client.zig").Request;
|
||||||
|
const Method = @import("../../http/Client.zig").Method;
|
||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
disable,
|
disable,
|
||||||
|
enable,
|
||||||
|
continueRequest,
|
||||||
|
failRequest,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
.disable => return cmd.sendResult(null, .{}),
|
.disable => return disable(cmd),
|
||||||
|
.enable => return enable(cmd),
|
||||||
|
.continueRequest => return continueRequest(cmd),
|
||||||
|
.failRequest => return failRequest(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stored in CDP
|
||||||
|
pub const InterceptState = struct {
|
||||||
|
const Self = @This();
|
||||||
|
waiting: std.AutoArrayHashMap(u64, Request),
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator) !InterceptState {
|
||||||
|
return .{
|
||||||
|
.waiting = std.AutoArrayHashMap(u64, Request).init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.waiting.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const RequestPattern = struct {
|
||||||
|
urlPattern: []const u8 = "*", // Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed. Escape character is backslash. Omitting is equivalent to "*".
|
||||||
|
resourceType: ?ResourceType = null,
|
||||||
|
requestStage: RequestStage = .Request,
|
||||||
|
};
|
||||||
|
const ResourceType = enum {
|
||||||
|
Document,
|
||||||
|
Stylesheet,
|
||||||
|
Image,
|
||||||
|
Media,
|
||||||
|
Font,
|
||||||
|
Script,
|
||||||
|
TextTrack,
|
||||||
|
XHR,
|
||||||
|
Fetch,
|
||||||
|
Prefetch,
|
||||||
|
EventSource,
|
||||||
|
WebSocket,
|
||||||
|
Manifest,
|
||||||
|
SignedExchange,
|
||||||
|
Ping,
|
||||||
|
CSPViolationReport,
|
||||||
|
Preflight,
|
||||||
|
FedCM,
|
||||||
|
Other,
|
||||||
|
};
|
||||||
|
const RequestStage = enum {
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
};
|
||||||
|
|
||||||
|
const EnableParam = struct {
|
||||||
|
patterns: []RequestPattern = &.{},
|
||||||
|
handleAuthRequests: bool = false,
|
||||||
|
};
|
||||||
|
const ErrorReason = enum {
|
||||||
|
Failed,
|
||||||
|
Aborted,
|
||||||
|
TimedOut,
|
||||||
|
AccessDenied,
|
||||||
|
ConnectionClosed,
|
||||||
|
ConnectionReset,
|
||||||
|
ConnectionRefused,
|
||||||
|
ConnectionAborted,
|
||||||
|
ConnectionFailed,
|
||||||
|
NameNotResolved,
|
||||||
|
InternetDisconnected,
|
||||||
|
AddressUnreachable,
|
||||||
|
BlockedByClient,
|
||||||
|
BlockedByResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn disable(cmd: anytype) !void {
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
bc.fetchDisable();
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enable(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(EnableParam)) orelse EnableParam{};
|
||||||
|
if (params.patterns.len != 0) log.warn(.cdp, "Fetch.enable No patterns yet", .{});
|
||||||
|
if (params.handleAuthRequests) log.warn(.cdp, "Fetch.enable No auth yet", .{});
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
try bc.fetchEnable();
|
||||||
|
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn requestPaused(arena: Allocator, bc: anytype, intercept: *const Notification.RequestIntercept) !void {
|
||||||
|
var cdp = bc.cdp;
|
||||||
|
|
||||||
|
// unreachable because we _have_ to have a page.
|
||||||
|
const session_id = bc.session_id orelse unreachable;
|
||||||
|
const target_id = bc.target_id orelse unreachable;
|
||||||
|
|
||||||
|
// We keep it around to wait for modifications to the request.
|
||||||
|
// NOTE: we assume whomever created the request created it with a lifetime of the Page.
|
||||||
|
// TODO: What to do when receiving replies for a previous page's requests?
|
||||||
|
|
||||||
|
try cdp.intercept_state.waiting.put(intercept.request.id.?, intercept.request.*);
|
||||||
|
|
||||||
|
// NOTE: .request data preparation is duped from network.zig
|
||||||
|
const full_request_url = try std.Uri.parse(intercept.request.url);
|
||||||
|
const request_url = try @import("network.zig").urlToString(arena, &full_request_url, .{
|
||||||
|
.scheme = true,
|
||||||
|
.authentication = true,
|
||||||
|
.authority = true,
|
||||||
|
.path = true,
|
||||||
|
.query = true,
|
||||||
|
});
|
||||||
|
const request_fragment = try @import("network.zig").urlToString(arena, &full_request_url, .{
|
||||||
|
.fragment = true,
|
||||||
|
});
|
||||||
|
const headers = try intercept.request.headers.asHashMap(arena);
|
||||||
|
// End of duped code
|
||||||
|
|
||||||
|
try cdp.sendEvent("Fetch.requestPaused", .{
|
||||||
|
.requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{intercept.request.id.?}),
|
||||||
|
.request = .{
|
||||||
|
.url = request_url,
|
||||||
|
.urlFragment = request_fragment,
|
||||||
|
.method = @tagName(intercept.request.method),
|
||||||
|
.hasPostData = intercept.request.body != null,
|
||||||
|
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
|
||||||
|
},
|
||||||
|
.frameId = target_id,
|
||||||
|
.resourceType = ResourceType.Document, // TODO!
|
||||||
|
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{intercept.request.id.?}),
|
||||||
|
}, .{ .session_id = session_id });
|
||||||
|
|
||||||
|
// Await either continueRequest, failRequest or fulfillRequest
|
||||||
|
intercept.wait_for_interception.* = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderEntry = struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn continueRequest(cmd: anytype) !void {
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
requestId: []const u8, // "INTERCEPT-{d}"
|
||||||
|
url: ?[]const u8 = null,
|
||||||
|
method: ?[]const u8 = null,
|
||||||
|
postData: ?[]const u8 = null,
|
||||||
|
headers: ?[]const HeaderEntry = null,
|
||||||
|
interceptResponse: bool = false,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
if (params.postData != null or params.headers != null or params.interceptResponse) return error.NotYetImplementedParams;
|
||||||
|
|
||||||
|
const request_id = try idFromRequestId(params.requestId);
|
||||||
|
var waiting_request = (bc.cdp.intercept_state.waiting.fetchSwapRemove(request_id) orelse return error.RequestNotFound).value;
|
||||||
|
|
||||||
|
// Update the request with the new parameters
|
||||||
|
if (params.url) |url| {
|
||||||
|
// The request url must be modified in a way that's not observable by page. So page.url is not updated.
|
||||||
|
waiting_request.url = try bc.cdp.browser.page_arena.allocator().dupeZ(u8, url);
|
||||||
|
}
|
||||||
|
if (params.method) |method| {
|
||||||
|
waiting_request.method = std.meta.stringToEnum(Method, method) orelse return error.InvalidParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(.cdp, "Request continued by intercept", .{ .id = params.requestId });
|
||||||
|
try bc.cdp.browser.http_client.request(waiting_request);
|
||||||
|
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn failRequest(cmd: anytype) !void {
|
||||||
|
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||||
|
var state = &bc.cdp.intercept_state;
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
requestId: []const u8, // "INTERCEPT-{d}"
|
||||||
|
errorReason: ErrorReason,
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
const request_id = try idFromRequestId(params.requestId);
|
||||||
|
if (state.waiting.fetchSwapRemove(request_id) == null) return error.RequestNotFound;
|
||||||
|
|
||||||
|
log.info(.cdp, "Request aborted by intercept", .{ .reason = params.errorReason });
|
||||||
|
return cmd.sendResult(null, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get u64 from requestId which is formatted as: "INTERCEPT-{d}"
|
||||||
|
fn idFromRequestId(request_id: []const u8) !u64 {
|
||||||
|
if (!std.mem.startsWith(u8, request_id, "INTERCEPT-")) return error.InvalidParams;
|
||||||
|
return std.fmt.parseInt(u64, request_id[10..], 10) catch return error.InvalidParams;
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, request: *const Notificati
|
|||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notification.RequestStart) !void {
|
pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification.RequestStart) !void {
|
||||||
// Isn't possible to do a network request within a Browser (which our
|
// Isn't possible to do a network request within a Browser (which our
|
||||||
// notification is tied to), without a page.
|
// notification is tied to), without a page.
|
||||||
std.debug.assert(bc.session.page != null);
|
std.debug.assert(bc.session.page != null);
|
||||||
@@ -251,36 +251,31 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notificat
|
|||||||
.query = true,
|
.query = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const request_url = try urlToString(arena, request.url, .{
|
const full_request_url = try std.Uri.parse(data.request.url);
|
||||||
|
const request_url = try urlToString(arena, &full_request_url, .{
|
||||||
.scheme = true,
|
.scheme = true,
|
||||||
.authentication = true,
|
.authentication = true,
|
||||||
.authority = true,
|
.authority = true,
|
||||||
.path = true,
|
.path = true,
|
||||||
.query = true,
|
.query = true,
|
||||||
});
|
});
|
||||||
|
const request_fragment = try urlToString(arena, &full_request_url, .{
|
||||||
const request_fragment = try urlToString(arena, request.url, .{
|
.fragment = true, // TODO since path is false, this likely does not work as intended
|
||||||
.fragment = true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// @newhttp
|
const headers = try data.request.headers.asHashMap(arena);
|
||||||
const headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty;
|
|
||||||
// try headers.ensureTotalCapacity(arena, request.headers.items.len);
|
|
||||||
// for (request.headers.items) |header| {
|
|
||||||
// headers.putAssumeCapacity(header.name, header.value);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// We're missing a bunch of fields, but, for now, this seems like enough
|
// We're missing a bunch of fields, but, for now, this seems like enough
|
||||||
try cdp.sendEvent("Network.requestWillBeSent", .{
|
try cdp.sendEvent("Network.requestWillBeSent", .{
|
||||||
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
|
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.request.id.?}),
|
||||||
.frameId = target_id,
|
.frameId = target_id,
|
||||||
.loaderId = bc.loader_id,
|
.loaderId = bc.loader_id,
|
||||||
.documentUrl = document_url,
|
.documentUrl = document_url,
|
||||||
.request = .{
|
.request = .{
|
||||||
.url = request_url,
|
.url = request_url,
|
||||||
.urlFragment = request_fragment,
|
.urlFragment = request_fragment,
|
||||||
.method = @tagName(request.method),
|
.method = @tagName(data.request.method),
|
||||||
.hasPostData = request.has_body,
|
.hasPostData = data.request.body != null,
|
||||||
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
|
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
|
||||||
},
|
},
|
||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
@@ -326,7 +321,7 @@ pub fn httpRequestComplete(arena: Allocator, bc: anytype, request: *const Notifi
|
|||||||
}, .{ .session_id = session_id });
|
}, .{ .session_id = session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn urlToString(arena: Allocator, url: *const std.Uri, opts: std.Uri.WriteToStreamOptions) ![]const u8 {
|
pub fn urlToString(arena: Allocator, url: *const std.Uri, opts: std.Uri.WriteToStreamOptions) ![]const u8 {
|
||||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||||
try url.writeToStream(opts, buf.writer(arena));
|
try url.writeToStream(opts, buf.writer(arena));
|
||||||
return buf.items;
|
return buf.items;
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const std = @import("std");
|
|||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Http = @import("Http.zig");
|
const Http = @import("Http.zig");
|
||||||
|
pub const Headers = Http.Headers;
|
||||||
|
const Notification = @import("../notification.zig").Notification;
|
||||||
|
|
||||||
const c = Http.c;
|
const c = Http.c;
|
||||||
|
|
||||||
@@ -57,6 +59,9 @@ multi: *c.CURLM,
|
|||||||
// of easys.
|
// of easys.
|
||||||
handles: Handles,
|
handles: Handles,
|
||||||
|
|
||||||
|
// Use to generate the next request ID
|
||||||
|
next_request_id: u64 = 0,
|
||||||
|
|
||||||
// When handles has no more available easys, requests get queued.
|
// When handles has no more available easys, requests get queued.
|
||||||
queue: RequestQueue,
|
queue: RequestQueue,
|
||||||
|
|
||||||
@@ -74,6 +79,9 @@ transfer_pool: std.heap.MemoryPool(Transfer),
|
|||||||
// see ScriptManager.blockingGet
|
// see ScriptManager.blockingGet
|
||||||
blocking: Handle,
|
blocking: Handle,
|
||||||
|
|
||||||
|
// To notify registered subscribers of events, the browser sets/nulls this for us.
|
||||||
|
notification: ?*Notification = null,
|
||||||
|
|
||||||
// The only place this is meant to be used is in `makeRequest` BEFORE `perform`
|
// The only place this is meant to be used is in `makeRequest` BEFORE `perform`
|
||||||
// is called. It is used to generate our Cookie header. It can be used for other
|
// is called. It is used to generate our Cookie header. It can be used for other
|
||||||
// purposes, but keep in mind that, while single-threaded, calls like makeRequest
|
// purposes, but keep in mind that, while single-threaded, calls like makeRequest
|
||||||
@@ -184,12 +192,26 @@ pub fn tick(self: *Client, timeout_ms: usize) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(self: *Client, req: Request) !void {
|
pub fn request(self: *Client, req: Request) !void {
|
||||||
|
var req_copy = req; // We need it mutable
|
||||||
|
|
||||||
|
if (req_copy.id == null) { // If the ID has already been set that means the request was previously intercepted
|
||||||
|
req_copy.id = self.next_request_id;
|
||||||
|
self.next_request_id += 1;
|
||||||
|
if (self.notification) |notification| {
|
||||||
|
notification.dispatch(.http_request_start, &.{ .request = &req_copy });
|
||||||
|
|
||||||
|
var wait_for_interception = false;
|
||||||
|
notification.dispatch(.http_request_intercept, &.{ .request = &req_copy, .wait_for_interception = &wait_for_interception });
|
||||||
|
if (wait_for_interception) return; // The user is send an invitation to intercept this request.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (self.handles.getFreeHandle()) |handle| {
|
if (self.handles.getFreeHandle()) |handle| {
|
||||||
return self.makeRequest(handle, req);
|
return self.makeRequest(handle, req_copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = try self.queue_node_pool.create();
|
const node = try self.queue_node_pool.create();
|
||||||
node.data = req;
|
node.data = req_copy;
|
||||||
self.queue.append(node);
|
self.queue.append(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +261,8 @@ fn makeRequest(self: *Client, handle: *Handle, req: Request) !void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const header_list = blk: {
|
var header_list = req.headers;
|
||||||
|
{
|
||||||
errdefer self.handles.release(handle);
|
errdefer self.handles.release(handle);
|
||||||
try conn.setMethod(req.method);
|
try conn.setMethod(req.method);
|
||||||
try conn.setURL(req.url);
|
try conn.setURL(req.url);
|
||||||
@@ -248,31 +271,23 @@ fn makeRequest(self: *Client, handle: *Handle, req: Request) !void {
|
|||||||
try conn.setBody(b);
|
try conn.setBody(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
var header_list = conn.commonHeaders();
|
// { // TODO move up to `fn request()`
|
||||||
errdefer c.curl_slist_free_all(header_list);
|
// const aa = self.arena.allocator();
|
||||||
|
// var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||||
|
// try req.cookie.forRequest(&uri, arr.writer(aa));
|
||||||
|
|
||||||
if (req.header) |hdr| {
|
// if (arr.items.len > 0) {
|
||||||
header_list = c.curl_slist_append(header_list, hdr);
|
// try arr.append(aa, 0); //null terminate
|
||||||
}
|
|
||||||
|
|
||||||
{
|
// // copies the value
|
||||||
const aa = self.arena.allocator();
|
// header_list = c.curl_slist_append(header_list, @ptrCast(arr.items.ptr));
|
||||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
// defer _ = self.arena.reset(.{ .retain_with_limit = 2048 });
|
||||||
try req.cookie.forRequest(&uri, arr.writer(aa));
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (arr.items.len > 0) {
|
try conn.secretHeaders(&header_list); // Add headers that must be hidden from intercepts
|
||||||
try arr.append(aa, 0); //null terminate
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list.headers));
|
||||||
|
}
|
||||||
// copies the value
|
|
||||||
header_list = c.curl_slist_append(header_list, @ptrCast(arr.items.ptr));
|
|
||||||
defer _ = self.arena.reset(.{ .retain_with_limit = 2048 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list));
|
|
||||||
|
|
||||||
break :blk header_list;
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
errdefer self.handles.release(handle);
|
errdefer self.handles.release(handle);
|
||||||
@@ -284,7 +299,7 @@ fn makeRequest(self: *Client, handle: *Handle, req: Request) !void {
|
|||||||
.req = req,
|
.req = req,
|
||||||
.ctx = req.ctx,
|
.ctx = req.ctx,
|
||||||
.handle = handle,
|
.handle = handle,
|
||||||
._request_header_list = header_list,
|
._request_header_list = header_list.headers,
|
||||||
};
|
};
|
||||||
errdefer self.transfer_pool.destroy(transfer);
|
errdefer self.transfer_pool.destroy(transfer);
|
||||||
|
|
||||||
@@ -471,10 +486,11 @@ pub const RequestCookie = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Request = struct {
|
pub const Request = struct {
|
||||||
|
id: ?u64 = null,
|
||||||
method: Method,
|
method: Method,
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
|
headers: Headers,
|
||||||
body: ?[]const u8 = null,
|
body: ?[]const u8 = null,
|
||||||
header: ?[:0]const u8 = null,
|
|
||||||
cookie: RequestCookie,
|
cookie: RequestCookie,
|
||||||
|
|
||||||
// arbitrary data that can be associated with this request
|
// arbitrary data that can be associated with this request
|
||||||
|
|||||||
@@ -185,20 +185,20 @@ pub const Connection = struct {
|
|||||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commonHeaders(self: *const Connection) *c.curl_slist {
|
// These are headers that may not be send to the users for inteception.
|
||||||
var header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0");
|
pub fn secretHeaders(self: *const Connection, headers: *Headers) !void {
|
||||||
if (self.opts.proxy_bearer_token) |hdr| {
|
if (self.opts.proxy_bearer_token) |hdr| {
|
||||||
header_list = c.curl_slist_append(header_list, hdr);
|
try headers.add(hdr);
|
||||||
}
|
}
|
||||||
return header_list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request(self: *const Connection) !u16 {
|
pub fn request(self: *const Connection) !u16 {
|
||||||
const easy = self.easy;
|
const easy = self.easy;
|
||||||
|
|
||||||
const header_list = self.commonHeaders();
|
const header_list = try Headers.init();
|
||||||
defer c.curl_slist_free_all(header_list);
|
defer header_list.deinit();
|
||||||
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list));
|
try self.secretHeaders(&header_list);
|
||||||
|
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list.headers));
|
||||||
|
|
||||||
try errorCheck(c.curl_easy_perform(easy));
|
try errorCheck(c.curl_easy_perform(easy));
|
||||||
var http_code: c_long = undefined;
|
var http_code: c_long = undefined;
|
||||||
@@ -210,6 +210,59 @@ pub const Connection = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Headers = struct {
|
||||||
|
headers: *c.curl_slist,
|
||||||
|
|
||||||
|
pub fn init() !Headers {
|
||||||
|
const header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0");
|
||||||
|
if (header_list == null) return error.OutOfMemory;
|
||||||
|
return .{ .headers = header_list };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Headers) void {
|
||||||
|
c.curl_slist_free_all(self.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: *Headers, header: [*c]const u8) !void {
|
||||||
|
const updated_headers = c.curl_slist_append(self.headers, header);
|
||||||
|
if (updated_headers == null) return error.OutOfMemory;
|
||||||
|
self.headers = updated_headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn asHashMap(self: *const Headers, allocator: Allocator) !std.StringArrayHashMapUnmanaged([]const u8) {
|
||||||
|
var list: std.StringArrayHashMapUnmanaged([]const u8) = .empty;
|
||||||
|
try list.ensureTotalCapacity(allocator, self.count());
|
||||||
|
|
||||||
|
var current: [*c]c.curl_slist = self.headers;
|
||||||
|
while (current) |node| {
|
||||||
|
const str = std.mem.span(@as([*:0]const u8, @ptrCast(node.*.data)));
|
||||||
|
const header = parseHeader(str) orelse return error.InvalidHeader;
|
||||||
|
list.putAssumeCapacity(header.name, header.value);
|
||||||
|
current = node.*.next;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseHeader(header_str: []const u8) ?struct { name: []const u8, value: []const u8 } {
|
||||||
|
const colon_pos = std.mem.indexOf(u8, header_str, ":") orelse return null;
|
||||||
|
|
||||||
|
const name = std.mem.trim(u8, header_str[0..colon_pos], " \t");
|
||||||
|
const value = std.mem.trim(u8, header_str[colon_pos + 1 ..], " \t");
|
||||||
|
|
||||||
|
return .{ .name = name, .value = value };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn count(self: *const Headers) usize {
|
||||||
|
var current: [*c]c.curl_slist = self.headers;
|
||||||
|
var num: usize = 0;
|
||||||
|
while (current) |node| {
|
||||||
|
num += 1;
|
||||||
|
current = node.*.next;
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn errorCheck(code: c.CURLcode) errors.Error!void {
|
pub fn errorCheck(code: c.CURLcode) errors.Error!void {
|
||||||
if (code == c.CURLE_OK) {
|
if (code == c.CURLE_OK) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const log = @import("log.zig");
|
|||||||
const URL = @import("url.zig").URL;
|
const URL = @import("url.zig").URL;
|
||||||
const page = @import("browser/page.zig");
|
const page = @import("browser/page.zig");
|
||||||
const Http = @import("http/Http.zig");
|
const Http = @import("http/Http.zig");
|
||||||
|
const Request = @import("http/Client.zig").Request;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ pub const Notification = struct {
|
|||||||
page_navigated: List = .{},
|
page_navigated: List = .{},
|
||||||
http_request_fail: List = .{},
|
http_request_fail: List = .{},
|
||||||
http_request_start: List = .{},
|
http_request_start: List = .{},
|
||||||
|
http_request_intercept: List = .{},
|
||||||
http_request_complete: List = .{},
|
http_request_complete: List = .{},
|
||||||
notification_created: List = .{},
|
notification_created: List = .{},
|
||||||
};
|
};
|
||||||
@@ -72,6 +74,7 @@ pub const Notification = struct {
|
|||||||
page_navigated: *const PageNavigated,
|
page_navigated: *const PageNavigated,
|
||||||
http_request_fail: *const RequestFail,
|
http_request_fail: *const RequestFail,
|
||||||
http_request_start: *const RequestStart,
|
http_request_start: *const RequestStart,
|
||||||
|
http_request_intercept: *const RequestIntercept,
|
||||||
http_request_complete: *const RequestComplete,
|
http_request_complete: *const RequestComplete,
|
||||||
notification_created: *Notification,
|
notification_created: *Notification,
|
||||||
};
|
};
|
||||||
@@ -91,11 +94,12 @@ pub const Notification = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const RequestStart = struct {
|
pub const RequestStart = struct {
|
||||||
arena: Allocator,
|
request: *Request,
|
||||||
id: usize,
|
};
|
||||||
url: *const std.Uri,
|
|
||||||
method: Http.Method,
|
pub const RequestIntercept = struct {
|
||||||
has_body: bool,
|
request: *Request,
|
||||||
|
wait_for_interception: *bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const RequestFail = struct {
|
pub const RequestFail = struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user