Skip to content

Commit b8fe5fb

Browse files
committed
intercept continue and abort
1 parent ae92ef6 commit b8fe5fb

File tree

6 files changed

+269
-19
lines changed

6 files changed

+269
-19
lines changed

src/browser/browser.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub const Browser = struct {
5252
errdefer env.deinit();
5353

5454
const notification = try Notification.init(allocator, app.notification);
55+
app.http.client.notification = notification;
5556
errdefer notification.deinit();
5657

5758
return .{

src/cdp/cdp.zig

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const Page = @import("../browser/page.zig").Page;
2929
const Inspector = @import("../browser/env.zig").Env.Inspector;
3030
const Incrementing = @import("../id.zig").Incrementing;
3131
const Notification = @import("../notification.zig").Notification;
32+
const InterceptState = @import("domains/fetch.zig").InterceptState;
3233

3334
const polyfill = @import("../browser/polyfill/polyfill.zig");
3435

@@ -75,6 +76,8 @@ pub fn CDPT(comptime TypeProvider: type) type {
7576
// Extra headers to add to all requests. TBD under which conditions this should be reset.
7677
extra_headers: std.ArrayListUnmanaged(std.http.Header) = .empty,
7778

79+
intercept_state: InterceptState,
80+
7881
const Self = @This();
7982

8083
pub fn init(app: *App, client: TypeProvider.Client) !Self {
@@ -89,13 +92,15 @@ pub fn CDPT(comptime TypeProvider: type) type {
8992
.browser_context = null,
9093
.message_arena = std.heap.ArenaAllocator.init(allocator),
9194
.notification_arena = std.heap.ArenaAllocator.init(allocator),
95+
.intercept_state = try InterceptState.init(allocator), // TBD or browser session arena?
9296
};
9397
}
9498

9599
pub fn deinit(self: *Self) void {
96100
if (self.browser_context) |*bc| {
97101
bc.deinit();
98102
}
103+
self.intercept_state.deinit(); // TBD Should this live in BC?
99104
self.browser.deinit();
100105
self.message_arena.deinit();
101106
self.notification_arena.deinit();
@@ -451,6 +456,14 @@ pub fn BrowserContext(comptime CDP_T: type) type {
451456
self.cdp.browser.notification.unregister(.http_request_complete, self);
452457
}
453458

459+
pub fn fetchEnable(self: *Self) !void {
460+
try self.cdp.browser.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
461+
}
462+
463+
pub fn fetchDisable(self: *Self) void {
464+
self.cdp.browser.notification.unregister(.http_request_intercept, self);
465+
}
466+
454467
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
455468
const self: *Self = @alignCast(@ptrCast(ctx));
456469
return @import("domains/page.zig").pageRemove(self);
@@ -475,7 +488,13 @@ pub fn BrowserContext(comptime CDP_T: type) type {
475488
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
476489
const self: *Self = @alignCast(@ptrCast(ctx));
477490
defer self.resetNotificationArena();
478-
return @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
491+
try @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
492+
}
493+
494+
pub fn onHttpRequestIntercept(ctx: *anyopaque, data: *const Notification.RequestIntercept) !void {
495+
const self: *Self = @alignCast(@ptrCast(ctx));
496+
defer self.resetNotificationArena();
497+
try @import("domains/fetch.zig").requestPaused(self.notification_arena, self, data);
479498
}
480499

481500
pub fn onHttpRequestFail(ctx: *anyopaque, data: *const Notification.RequestFail) !void {

src/cdp/domains/fetch.zig

Lines changed: 206 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
1+
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
22
//
33
// Francis Bouvier <[email protected]>
44
// Pierre Tachoire <[email protected]>
@@ -17,13 +17,217 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const Allocator = std.mem.Allocator;
21+
const Notification = @import("../../notification.zig").Notification;
22+
const log = @import("../../log.zig");
23+
const Request = @import("../../http/Client.zig").Request;
24+
const Method = @import("../../http/Client.zig").Method;
2025

2126
pub fn processMessage(cmd: anytype) !void {
2227
const action = std.meta.stringToEnum(enum {
2328
disable,
29+
enable,
30+
continueRequest,
31+
failRequest,
2432
}, cmd.input.action) orelse return error.UnknownMethod;
2533

2634
switch (action) {
27-
.disable => return cmd.sendResult(null, .{}),
35+
.disable => return disable(cmd),
36+
.enable => return enable(cmd),
37+
.continueRequest => return continueRequest(cmd),
38+
.failRequest => return failRequest(cmd),
2839
}
2940
}
41+
42+
// Stored in CDP
43+
pub const InterceptState = struct {
44+
const Self = @This();
45+
waiting: std.AutoArrayHashMap(u64, Request),
46+
47+
pub fn init(allocator: Allocator) !InterceptState {
48+
return .{
49+
.waiting = std.AutoArrayHashMap(u64, Request).init(allocator),
50+
};
51+
}
52+
53+
pub fn deinit(self: *Self) void {
54+
self.waiting.deinit();
55+
}
56+
};
57+
58+
const RequestPattern = struct {
59+
urlPattern: []const u8 = "*", // Wildcards ('*' -> zero or more, '?' -> exactly one) are allowed. Escape character is backslash. Omitting is equivalent to "*".
60+
resourceType: ?ResourceType = null,
61+
requestStage: RequestStage = .Request,
62+
};
63+
const ResourceType = enum {
64+
Document,
65+
Stylesheet,
66+
Image,
67+
Media,
68+
Font,
69+
Script,
70+
TextTrack,
71+
XHR,
72+
Fetch,
73+
Prefetch,
74+
EventSource,
75+
WebSocket,
76+
Manifest,
77+
SignedExchange,
78+
Ping,
79+
CSPViolationReport,
80+
Preflight,
81+
FedCM,
82+
Other,
83+
};
84+
const RequestStage = enum {
85+
Request,
86+
Response,
87+
};
88+
89+
const EnableParam = struct {
90+
patterns: []RequestPattern = &.{},
91+
handleAuthRequests: bool = false,
92+
};
93+
const ErrorReason = enum {
94+
Failed,
95+
Aborted,
96+
TimedOut,
97+
AccessDenied,
98+
ConnectionClosed,
99+
ConnectionReset,
100+
ConnectionRefused,
101+
ConnectionAborted,
102+
ConnectionFailed,
103+
NameNotResolved,
104+
InternetDisconnected,
105+
AddressUnreachable,
106+
BlockedByClient,
107+
BlockedByResponse,
108+
};
109+
110+
fn disable(cmd: anytype) !void {
111+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
112+
bc.fetchDisable();
113+
return cmd.sendResult(null, .{});
114+
}
115+
116+
fn enable(cmd: anytype) !void {
117+
const params = (try cmd.params(EnableParam)) orelse EnableParam{};
118+
if (params.patterns.len != 0) std.debug.print("Fetch.enable: Not implemented patterns set\n", .{});
119+
if (params.handleAuthRequests) std.debug.print("Fetch.enable: Not implemented handleAuthRequests set\n", .{});
120+
121+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
122+
try bc.fetchEnable();
123+
124+
return cmd.sendResult(null, .{});
125+
}
126+
127+
pub fn requestPaused(arena: Allocator, bc: anytype, intercept: *const Notification.RequestIntercept) !void {
128+
var cdp = bc.cdp;
129+
130+
// unreachable because we _have_ to have a page.
131+
const session_id = bc.session_id orelse unreachable;
132+
const target_id = bc.target_id orelse unreachable;
133+
134+
// We keep it around to wait for modifications to the request.
135+
// NOTE: we assume whomever created the request created it with a lifetime of the Page.
136+
// TODO: What to do when receiving replies for a previous page's requests?
137+
138+
try cdp.intercept_state.waiting.put(intercept.request.id.?, intercept.request.*);
139+
140+
// NOTE: .request data preparation is duped from network.zig
141+
const full_request_url = try std.Uri.parse(intercept.request.url);
142+
const request_url = try @import("network.zig").urlToString(arena, &full_request_url, .{
143+
.scheme = true,
144+
.authentication = true,
145+
.authority = true,
146+
.path = true,
147+
.query = true,
148+
});
149+
const request_fragment = try @import("network.zig").urlToString(arena, &full_request_url, .{
150+
.fragment = true,
151+
});
152+
const headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty;
153+
// try headers.ensureTotalCapacity(arena, request.headers.items.len);
154+
// for (request.headers.items) |header| {
155+
// headers.putAssumeCapacity(header.name, header.value);
156+
// }
157+
// End of duped code
158+
159+
try cdp.sendEvent("Fetch.requestPaused", .{
160+
.requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{intercept.request.id.?}),
161+
.request = .{
162+
.url = request_url,
163+
.urlFragment = request_fragment,
164+
.method = @tagName(intercept.request.method),
165+
.hasPostData = intercept.request.body != null,
166+
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
167+
},
168+
.frameId = target_id,
169+
.resourceType = ResourceType.Document, // TODO!
170+
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{intercept.request.id.?}),
171+
}, .{ .session_id = session_id });
172+
173+
// TODO await either continueRequest, failRequest or fulfillRequest
174+
intercept.wait_for_interception.* = true;
175+
}
176+
177+
const HeaderEntry = struct {
178+
name: []const u8,
179+
value: []const u8,
180+
};
181+
182+
fn continueRequest(cmd: anytype) !void {
183+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
184+
const params = (try cmd.params(struct {
185+
requestId: []const u8, // "INTERCEPT-{d}"
186+
url: ?[]const u8 = null,
187+
method: ?[]const u8 = null,
188+
postData: ?[]const u8 = null,
189+
headers: ?[]const HeaderEntry = null,
190+
interceptResponse: bool = false,
191+
})) orelse return error.InvalidParams;
192+
if (params.postData != null or params.headers != null or params.interceptResponse) return error.NotYetImplementedParams;
193+
194+
const request_id = try id_from_request_id(params.requestId);
195+
var waiting_request = (bc.cdp.intercept_state.waiting.fetchSwapRemove(request_id) orelse return error.RequestNotFound).value;
196+
197+
// Update the request with the new parameters
198+
if (params.url) |url| {
199+
// The request url must be modified in a way that's not observable by page. So page.url is not updated.
200+
waiting_request.url = try bc.cdp.browser.page_arena.allocator().dupeZ(u8, url);
201+
}
202+
if (params.method) |method| {
203+
waiting_request.method = std.meta.stringToEnum(Method, method) orelse return error.InvalidParams;
204+
}
205+
206+
log.info(.cdp, "Request continued by intercept", .{params.requestId});
207+
try bc.cdp.browser.http_client.request(waiting_request);
208+
209+
return cmd.sendResult(null, .{});
210+
}
211+
212+
fn failRequest(cmd: anytype) !void {
213+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
214+
var state = &bc.cdp.intercept_state;
215+
const params = (try cmd.params(struct {
216+
requestId: []const u8, // "INTERCEPT-{d}"
217+
errorReason: ErrorReason,
218+
})) orelse return error.InvalidParams;
219+
220+
const request_id = try id_from_request_id(params.requestId);
221+
if (state.waiting.fetchSwapRemove(request_id) == null) return error.RequestNotFound;
222+
223+
log.info(.cdp, "Request aborted by intercept", .{params.errorReason});
224+
return cmd.sendResult(null, .{});
225+
}
226+
227+
// Get u64 from requestId which is formatted as: "INTERCEPT-{d}"
228+
fn id_from_request_id(request_id: []const u8) !u64 {
229+
return std.fmt.parseInt(u64, request_id[10..], 10) catch |err| {
230+
log.err(.cdp, "Failed to parse requestId", .{request_id});
231+
return err;
232+
};
233+
}

src/cdp/domains/network.zig

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ pub fn httpRequestFail(arena: Allocator, bc: anytype, request: *const Notificati
223223
}, .{ .session_id = session_id });
224224
}
225225

226-
pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notification.RequestStart) !void {
226+
pub fn httpRequestStart(arena: Allocator, bc: anytype, data: *const Notification.RequestStart) !void {
227227
// Isn't possible to do a network request within a Browser (which our
228228
// notification is tied to), without a page.
229229
std.debug.assert(bc.session.page != null);
@@ -251,16 +251,16 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notificat
251251
.query = true,
252252
});
253253

254-
const request_url = try urlToString(arena, request.url, .{
254+
const full_request_url = try std.Uri.parse(data.request.url);
255+
const request_url = try urlToString(arena, &full_request_url, .{
255256
.scheme = true,
256257
.authentication = true,
257258
.authority = true,
258259
.path = true,
259260
.query = true,
260261
});
261-
262-
const request_fragment = try urlToString(arena, request.url, .{
263-
.fragment = true,
262+
const request_fragment = try urlToString(arena, &full_request_url, .{
263+
.fragment = true, // TODO since path is false, this likely does not work as intended
264264
});
265265

266266
// @newhttp
@@ -272,15 +272,15 @@ pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notificat
272272

273273
// We're missing a bunch of fields, but, for now, this seems like enough
274274
try cdp.sendEvent("Network.requestWillBeSent", .{
275-
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
275+
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{data.request.id.?}),
276276
.frameId = target_id,
277277
.loaderId = bc.loader_id,
278278
.documentUrl = document_url,
279279
.request = .{
280280
.url = request_url,
281281
.urlFragment = request_fragment,
282-
.method = @tagName(request.method),
283-
.hasPostData = request.has_body,
282+
.method = @tagName(data.request.method),
283+
.hasPostData = data.request.body != null,
284284
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
285285
},
286286
}, .{ .session_id = session_id });
@@ -326,7 +326,7 @@ pub fn httpRequestComplete(arena: Allocator, bc: anytype, request: *const Notifi
326326
}, .{ .session_id = session_id });
327327
}
328328

329-
fn urlToString(arena: Allocator, url: *const std.Uri, opts: std.Uri.WriteToStreamOptions) ![]const u8 {
329+
pub fn urlToString(arena: Allocator, url: *const std.Uri, opts: std.Uri.WriteToStreamOptions) ![]const u8 {
330330
var buf: std.ArrayListUnmanaged(u8) = .empty;
331331
try url.writeToStream(opts, buf.writer(arena));
332332
return buf.items;

0 commit comments

Comments
 (0)