Skip to content

WIP: auth required interception #960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: proxy-header
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/cdp/cdp.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 });
}
Expand Down
148 changes: 143 additions & 5 deletions src/cdp/domains/fetch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -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, .{});
}
Expand Down Expand Up @@ -276,6 +274,60 @@ 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,
password: ?[]const u8,
},
})) 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.abort();
transfer.deinit();
return cmd.sendResult(null, .{});
}

// cancel the request, deinit the transfer on error.
errdefer {
transfer.abort();
transfer.deinit();
}

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;

Expand Down Expand Up @@ -346,6 +398,92 @@ fn failRequest(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

const AuthChallenge = struct {
scheme: enum { basic, digest },
realm: []const u8,

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
// Supports only basic and digest schemes.
pub fn parse(header: []const u8) !AuthChallenge {
var ac: AuthChallenge = .{
.scheme = undefined,
.realm = "",
};

const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, header, std.ascii.whitespace[0..]), 0, " ") orelse header.len;
const _scheme = header[0..pos];
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
ac.scheme = .basic;
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
ac.scheme = .digest;
} else {
return error.UnknownAuthChallengeScheme;
}

// TODO get the realm

return ac;
}
};

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);

var challenge: AuthChallenge = undefined;
var source: enum { server, proxy } = undefined;
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
if (std.ascii.eqlIgnoreCase("WWW-Authenticate", hdr.name)) {
source = .server;
challenge = try AuthChallenge.parse(hdr.value);
break;
}
if (std.ascii.eqlIgnoreCase("Proxy-Authenticate", hdr.name)) {
source = .proxy;
challenge = try AuthChallenge.parse(hdr.value);
break;
}
}

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 (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-")) {
Expand Down
32 changes: 30 additions & 2 deletions src/http/Client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
}

try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer));

// add credentials
if (req.credentials) |creds| {
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXYUSERPWD, creds.ptr));
}
}

// Once soon as this is called, our "perform" loop is responsible for
Expand Down Expand Up @@ -365,13 +370,31 @@ fn perform(self: *Client, timeout_ms: c_int) !void {
const easy = msg.easy_handle.?;
const transfer = try Transfer.fromEasy(easy);

// In case of forbidden
if (transfer._forbidden) {
if (transfer.client.notification) |notification| {
var wait_for_interception = false;
notification.dispatch(.http_request_auth_required, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
if (wait_for_interception) {
log.debug(.http, "WAIT FOR INTERCEPT", .{});
// the request is put on hold to be intercepted.
// In this case we ignore callbacks for now.
// Note: we don't deinit transfer on purpose: we want to keep
// using it for the following request.
self.endTransfer(transfer);
continue;
}
}
}

// release it ASAP so that it's available; some done_callbacks
// will load more resources.
self.endTransfer(transfer);

defer transfer.deinit();

if (errorCheck(msg.data.result)) {

// In case of request w/o data, we need to call the header done
// callback now.
if (!transfer._header_done_called) {
Expand Down Expand Up @@ -542,6 +565,7 @@ pub const Request = struct {
body: ?[]const u8 = null,
cookie_jar: *CookieJar,
resource_type: ResourceType,
credentials: ?[:0]const u8 = null,

// arbitrary data that can be associated with this request
ctx: *anyopaque = undefined,
Expand Down Expand Up @@ -584,7 +608,7 @@ pub const Transfer = struct {
_redirecting: bool = false,
_forbidden: bool = false,

fn deinit(self: *Transfer) void {
pub fn deinit(self: *Transfer) void {
self.req.headers.deinit();
if (self._handle) |handle| {
self.client.handles.release(handle);
Expand Down Expand Up @@ -633,6 +657,10 @@ pub const Transfer = struct {
self.req.url = url;
}

pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void {
self.req.credentials = userpwd;
}

pub fn replaceRequestHeaders(self: *Transfer, allocator: Allocator, headers: []const Http.Header) !void {
self.req.headers.deinit();

Expand Down Expand Up @@ -823,7 +851,7 @@ pub const Transfer = struct {
return c.CURL_WRITEFUNC_ERROR;
};

if (transfer._redirecting) {
if (transfer._redirecting or transfer._forbidden) {
return chunk_len;
}

Expand Down
2 changes: 1 addition & 1 deletion src/http/Http.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub const c = @cImport({
@cInclude("curl/curl.h");
});

pub const ENABLE_DEBUG = false;
pub const ENABLE_DEBUG = true;
pub const Client = @import("Client.zig");
pub const Transfer = Client.Transfer;

Expand Down
7 changes: 7 additions & 0 deletions src/notification.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = .{},
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down