cdp: add auth required interception process

This commit is contained in:
Pierre Tachoire
2025-08-26 10:52:41 +02:00
parent a847a1faae
commit 6b47aa2446
3 changed files with 115 additions and 6 deletions

View File

@@ -477,12 +477,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
self.cdp.browser.notification.unregister(.http_response_header_done, self);
}
pub fn fetchEnable(self: *Self) !void {
pub fn fetchEnable(self: *Self, authRequests: bool) !void {
try self.cdp.browser.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
if (authRequests) {
try self.cdp.browser.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);
}
}
pub fn fetchDisable(self: *Self) void {
self.cdp.browser.notification.unregister(.http_request_intercept, self);
self.cdp.browser.notification.unregister(.http_request_auth_required, self);
}
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
@@ -548,6 +552,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
}
pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
try @import("domains/fetch.zig").requestAuthRequired(self.notification_arena, self, data);
}
fn resetNotificationArena(self: *Self) void {
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
}

View File

@@ -32,12 +32,14 @@ pub fn processMessage(cmd: anytype) !void {
continueRequest,
failRequest,
fulfillRequest,
continueWithAuth,
}, cmd.input.action) orelse return error.UnknownMethod;
switch (action) {
.disable => return disable(cmd),
.enable => return enable(cmd),
.continueRequest => return continueRequest(cmd),
.continueWithAuth => return continueWithAuth(cmd),
.failRequest => return failRequest(cmd),
.fulfillRequest => return fulfillRequest(cmd),
}
@@ -144,12 +146,8 @@ fn enable(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
if (params.handleAuthRequests) {
log.warn(.cdp, "not implemented", .{ .feature = "Fetch.enable handleAuthRequests is not supported yet" });
}
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.fetchEnable();
try bc.fetchEnable(params.handleAuthRequests);
return cmd.sendResult(null, .{});
}
@@ -276,6 +274,56 @@ fn continueRequest(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn continueWithAuth(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INTERCEPT-{d}"
authChallengeResponse: struct {
response: []const u8,
username: ?[]const u8 = null,
password: ?[]const u8 = null,
},
})) orelse return error.InvalidParams;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
var intercept_state = &bc.intercept_state;
const request_id = try idFromRequestId(params.requestId);
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
log.debug(.cdp, "request intercept", .{
.state = "continue with auth",
.id = transfer.id,
.response = params.authChallengeResponse.response,
});
if (!std.mem.eql(u8, params.authChallengeResponse.response, "ProvideCredentials")) {
transfer.abortAuthChallenge();
return cmd.sendResult(null, .{});
}
// cancel the request, deinit the transfer on error.
errdefer transfer.abortAuthChallenge();
const username = params.authChallengeResponse.username orelse "";
const password = params.authChallengeResponse.password orelse "";
// restart the request with the provided credentials.
// we need to duplicate the cre
const arena = transfer.arena.allocator();
transfer.updateCredentials(
try std.fmt.allocPrintZ(arena, "{s}:{s}", .{ username, password }),
);
try bc.cdp.browser.http_client.process(transfer);
if (intercept_state.empty()) {
page.request_intercepted = false;
}
return cmd.sendResult(null, .{});
}
fn fulfillRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
@@ -346,6 +394,50 @@ fn failRequest(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {
// unreachable because we _have_ to have a page.
const session_id = bc.session_id orelse unreachable;
const target_id = bc.target_id orelse unreachable;
const page = bc.session.currentPage() 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?
const transfer = intercept.transfer;
try bc.intercept_state.put(transfer);
const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge;
try bc.cdp.sendEvent("Fetch.authRequired", .{
.requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{transfer.id}),
.request = network.TransferAsRequestWriter.init(transfer),
.frameId = target_id,
.resourceType = switch (transfer.req.resource_type) {
.script => "Script",
.xhr => "XHR",
.document => "Document",
},
.authChallenge = .{
.source = if (challenge.source == .server) "Server" else "Proxy",
.origin = "", // TODO get origin, could be the proxy address for example.
.scheme = if (challenge.scheme == .digest) "digest" else "basic",
.realm = challenge.realm,
},
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
}, .{ .session_id = session_id });
log.debug(.cdp, "request auth required", .{
.state = "paused",
.id = transfer.id,
.url = transfer.uri,
});
// Await continueWithAuth
intercept.wait_for_interception.* = true;
page.request_intercepted = true;
}
// 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-")) {

View File

@@ -64,6 +64,7 @@ pub const Notification = struct {
http_request_start: List = .{},
http_request_intercept: List = .{},
http_request_done: List = .{},
http_request_auth_required: List = .{},
http_response_data: List = .{},
http_response_header_done: List = .{},
notification_created: List = .{},
@@ -77,6 +78,7 @@ pub const Notification = struct {
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
http_request_auth_required: *const RequestAuthRequired,
http_request_done: *const RequestDone,
http_response_data: *const ResponseData,
http_response_header_done: *const ResponseHeaderDone,
@@ -106,6 +108,11 @@ pub const Notification = struct {
wait_for_interception: *bool,
};
pub const RequestAuthRequired = struct {
transfer: *Transfer,
wait_for_interception: *bool,
};
pub const ResponseData = struct {
data: []const u8,
transfer: *Transfer,