diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index af6ddafb..21817c66 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -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 }); } diff --git a/src/cdp/domains/fetch.zig b/src/cdp/domains/fetch.zig index 9422e178..e0b978c0 100644 --- a/src/cdp/domains/fetch.zig +++ b/src/cdp/domains/fetch.zig @@ -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-")) { diff --git a/src/notification.zig b/src/notification.zig index 43707e40..392a57e7 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -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,