Skip to content

Commit 7647ce9

Browse files
Merge pull request #960 from lightpanda-io/auth-challenge
auth required interception
2 parents 545d3f8 + 5defb5c commit 7647ce9

File tree

4 files changed

+245
-13
lines changed

4 files changed

+245
-13
lines changed

src/cdp/cdp.zig

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,12 +474,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
474474
self.cdp.browser.notification.unregister(.http_response_header_done, self);
475475
}
476476

477-
pub fn fetchEnable(self: *Self) !void {
477+
pub fn fetchEnable(self: *Self, authRequests: bool) !void {
478478
try self.cdp.browser.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
479+
if (authRequests) {
480+
try self.cdp.browser.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);
481+
}
479482
}
480483

481484
pub fn fetchDisable(self: *Self) void {
482485
self.cdp.browser.notification.unregister(.http_request_intercept, self);
486+
self.cdp.browser.notification.unregister(.http_request_auth_required, self);
483487
}
484488

485489
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
@@ -545,6 +549,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
545549
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
546550
}
547551

552+
pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void {
553+
const self: *Self = @alignCast(@ptrCast(ctx));
554+
defer self.resetNotificationArena();
555+
try @import("domains/fetch.zig").requestAuthRequired(self.notification_arena, self, data);
556+
}
557+
548558
fn resetNotificationArena(self: *Self) void {
549559
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
550560
}

src/cdp/domains/fetch.zig

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ pub fn processMessage(cmd: anytype) !void {
3232
continueRequest,
3333
failRequest,
3434
fulfillRequest,
35+
continueWithAuth,
3536
}, cmd.input.action) orelse return error.UnknownMethod;
3637

3738
switch (action) {
3839
.disable => return disable(cmd),
3940
.enable => return enable(cmd),
4041
.continueRequest => return continueRequest(cmd),
42+
.continueWithAuth => return continueWithAuth(cmd),
4143
.failRequest => return failRequest(cmd),
4244
.fulfillRequest => return fulfillRequest(cmd),
4345
}
@@ -144,12 +146,8 @@ fn enable(cmd: anytype) !void {
144146
return cmd.sendResult(null, .{});
145147
}
146148

147-
if (params.handleAuthRequests) {
148-
log.warn(.cdp, "not implemented", .{ .feature = "Fetch.enable handleAuthRequests is not supported yet" });
149-
}
150-
151149
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
152-
try bc.fetchEnable();
150+
try bc.fetchEnable(params.handleAuthRequests);
153151

154152
return cmd.sendResult(null, .{});
155153
}
@@ -276,6 +274,63 @@ fn continueRequest(cmd: anytype) !void {
276274
return cmd.sendResult(null, .{});
277275
}
278276

277+
// https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#type-AuthChallengeResponse
278+
const AuthChallengeResponse = enum {
279+
Default,
280+
CancelAuth,
281+
ProvideCredentials,
282+
};
283+
284+
fn continueWithAuth(cmd: anytype) !void {
285+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
286+
const params = (try cmd.params(struct {
287+
requestId: []const u8, // "INTERCEPT-{d}"
288+
authChallengeResponse: struct {
289+
response: AuthChallengeResponse,
290+
username: []const u8 = "",
291+
password: []const u8 = "",
292+
},
293+
})) orelse return error.InvalidParams;
294+
295+
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
296+
297+
var intercept_state = &bc.intercept_state;
298+
const request_id = try idFromRequestId(params.requestId);
299+
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
300+
301+
log.debug(.cdp, "request intercept", .{
302+
.state = "continue with auth",
303+
.id = transfer.id,
304+
.response = params.authChallengeResponse.response,
305+
});
306+
307+
if (params.authChallengeResponse.response != .ProvideCredentials) {
308+
transfer.abortAuthChallenge();
309+
return cmd.sendResult(null, .{});
310+
}
311+
312+
// cancel the request, deinit the transfer on error.
313+
errdefer transfer.abortAuthChallenge();
314+
315+
// restart the request with the provided credentials.
316+
const arena = transfer.arena.allocator();
317+
transfer.updateCredentials(
318+
try std.fmt.allocPrintZ(arena, "{s}:{s}", .{
319+
params.authChallengeResponse.username,
320+
params.authChallengeResponse.password,
321+
}),
322+
);
323+
324+
transfer.reset();
325+
try bc.cdp.browser.http_client.process(transfer);
326+
327+
if (intercept_state.empty()) {
328+
page.request_intercepted = false;
329+
}
330+
331+
return cmd.sendResult(null, .{});
332+
}
333+
279334
fn fulfillRequest(cmd: anytype) !void {
280335
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
281336

@@ -346,6 +401,50 @@ fn failRequest(cmd: anytype) !void {
346401
return cmd.sendResult(null, .{});
347402
}
348403

404+
pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {
405+
// unreachable because we _have_ to have a page.
406+
const session_id = bc.session_id orelse unreachable;
407+
const target_id = bc.target_id orelse unreachable;
408+
const page = bc.session.currentPage() orelse unreachable;
409+
410+
// We keep it around to wait for modifications to the request.
411+
// NOTE: we assume whomever created the request created it with a lifetime of the Page.
412+
// TODO: What to do when receiving replies for a previous page's requests?
413+
414+
const transfer = intercept.transfer;
415+
try bc.intercept_state.put(transfer);
416+
417+
const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge;
418+
419+
try bc.cdp.sendEvent("Fetch.authRequired", .{
420+
.requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{transfer.id}),
421+
.request = network.TransferAsRequestWriter.init(transfer),
422+
.frameId = target_id,
423+
.resourceType = switch (transfer.req.resource_type) {
424+
.script => "Script",
425+
.xhr => "XHR",
426+
.document => "Document",
427+
},
428+
.authChallenge = .{
429+
.source = if (challenge.source == .server) "Server" else "Proxy",
430+
.origin = "", // TODO get origin, could be the proxy address for example.
431+
.scheme = if (challenge.scheme == .digest) "digest" else "basic",
432+
.realm = challenge.realm,
433+
},
434+
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
435+
}, .{ .session_id = session_id });
436+
437+
log.debug(.cdp, "request auth required", .{
438+
.state = "paused",
439+
.id = transfer.id,
440+
.url = transfer.uri,
441+
});
442+
// Await continueWithAuth
443+
444+
intercept.wait_for_interception.* = true;
445+
page.request_intercepted = true;
446+
}
447+
349448
// Get u64 from requestId which is formatted as: "INTERCEPT-{d}"
350449
fn idFromRequestId(request_id: []const u8) !u64 {
351450
if (!std.mem.startsWith(u8, request_id, "INTERCEPT-")) {

src/http/Client.zig

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
328328
}
329329

330330
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer));
331+
332+
// add credentials
333+
if (req.credentials) |creds| {
334+
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXYUSERPWD, creds.ptr));
335+
}
331336
}
332337

333338
// Once soon as this is called, our "perform" loop is responsible for
@@ -378,6 +383,22 @@ fn perform(self: *Client, timeout_ms: c_int, socket: ?posix.socket_t) !bool {
378383
const easy = msg.easy_handle.?;
379384
const transfer = try Transfer.fromEasy(easy);
380385

386+
// In case of auth challenge
387+
if (transfer._auth_challenge != null and transfer._tries < 10) { // TODO give a way to configure the number of auth retries.
388+
if (transfer.client.notification) |notification| {
389+
var wait_for_interception = false;
390+
notification.dispatch(.http_request_auth_required, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
391+
if (wait_for_interception) {
392+
// the request is put on hold to be intercepted.
393+
// In this case we ignore callbacks for now.
394+
// Note: we don't deinit transfer on purpose: we want to keep
395+
// using it for the following request.
396+
self.endTransfer(transfer);
397+
continue;
398+
}
399+
}
400+
}
401+
381402
// release it ASAP so that it's available; some done_callbacks
382403
// will load more resources.
383404
self.endTransfer(transfer);
@@ -557,6 +578,7 @@ pub const Request = struct {
557578
body: ?[]const u8 = null,
558579
cookie_jar: *CookieJar,
559580
resource_type: ResourceType,
581+
credentials: ?[:0]const u8 = null,
560582

561583
// arbitrary data that can be associated with this request
562584
ctx: *anyopaque = undefined,
@@ -574,6 +596,46 @@ pub const Request = struct {
574596
};
575597
};
576598

599+
pub const AuthChallenge = struct {
600+
status: u16,
601+
source: enum { server, proxy },
602+
scheme: enum { basic, digest },
603+
realm: []const u8,
604+
605+
pub fn parse(status: u16, header: []const u8) !AuthChallenge {
606+
var ac: AuthChallenge = .{
607+
.status = status,
608+
.source = undefined,
609+
.realm = "TODO", // TODO parser and set realm
610+
.scheme = undefined,
611+
};
612+
613+
const sep = std.mem.indexOfPos(u8, header, 0, ": ") orelse return error.InvalidHeader;
614+
const hname = header[0..sep];
615+
const hvalue = header[sep + 2 ..];
616+
617+
if (std.ascii.eqlIgnoreCase("WWW-Authenticate", hname)) {
618+
ac.source = .server;
619+
} else if (std.ascii.eqlIgnoreCase("Proxy-Authenticate", hname)) {
620+
ac.source = .proxy;
621+
} else {
622+
return error.InvalidAuthChallenge;
623+
}
624+
625+
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, hvalue, std.ascii.whitespace[0..]), 0, " ") orelse hvalue.len;
626+
const _scheme = hvalue[0..pos];
627+
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
628+
ac.scheme = .basic;
629+
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
630+
ac.scheme = .digest;
631+
} else {
632+
return error.UnknownAuthChallengeScheme;
633+
}
634+
635+
return ac;
636+
}
637+
};
638+
577639
pub const Transfer = struct {
578640
arena: ArenaAllocator,
579641
id: usize = 0,
@@ -586,7 +648,6 @@ pub const Transfer = struct {
586648
bytes_received: usize = 0,
587649

588650
// We'll store the response header here
589-
proxy_response_header: ?ResponseHeader = null,
590651
response_header: ?ResponseHeader = null,
591652

592653
// track if the header callbacks done have been called.
@@ -597,7 +658,22 @@ pub const Transfer = struct {
597658
_handle: ?*Handle = null,
598659

599660
_redirecting: bool = false,
600-
_forbidden: bool = false,
661+
_auth_challenge: ?AuthChallenge = null,
662+
663+
// number of times the transfer has been tried.
664+
// incremented by reset func.
665+
_tries: u8 = 0,
666+
667+
pub fn reset(self: *Transfer) void {
668+
self._redirecting = false;
669+
self._auth_challenge = null;
670+
self._notified_fail = false;
671+
self._header_done_called = false;
672+
self.response_header = null;
673+
self.bytes_received = 0;
674+
675+
self._tries += 1;
676+
}
601677

602678
fn deinit(self: *Transfer) void {
603679
self.req.headers.deinit();
@@ -615,7 +691,11 @@ pub const Transfer = struct {
615691
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_EFFECTIVE_URL, &url));
616692

617693
var status: c_long = undefined;
618-
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &status));
694+
if (self._auth_challenge) |_| {
695+
status = 407;
696+
} else {
697+
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &status));
698+
}
619699

620700
self.response_header = .{
621701
.url = url,
@@ -648,6 +728,10 @@ pub const Transfer = struct {
648728
self.req.url = url;
649729
}
650730

731+
pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void {
732+
self.req.credentials = userpwd;
733+
}
734+
651735
pub fn replaceRequestHeaders(self: *Transfer, allocator: Allocator, headers: []const Http.Header) !void {
652736
self.req.headers.deinit();
653737

@@ -672,6 +756,14 @@ pub const Transfer = struct {
672756
self.deinit();
673757
}
674758

759+
// abortAuthChallenge is called when an auth chanllenge interception is
760+
// abort. We don't call self.client.endTransfer here b/c it has been done
761+
// before interception process.
762+
pub fn abortAuthChallenge(self: *Transfer) void {
763+
self.client.requestFailed(self, error.AbortAuthChallenge);
764+
self.deinit();
765+
}
766+
675767
// redirectionCookies manages cookies during redirections handled by Curl.
676768
// It sets the cookies from the current response to the cookie jar.
677769
// It also immediately sets cookies for the following request.
@@ -797,20 +889,44 @@ pub const Transfer = struct {
797889
transfer._redirecting = false;
798890

799891
if (status == 401 or status == 407) {
800-
transfer._forbidden = true;
892+
// The auth challenge must be parsed from a following
893+
// WWW-Authenticate or Proxy-Authenticate header.
894+
transfer._auth_challenge = .{
895+
.status = status,
896+
.source = undefined,
897+
.scheme = undefined,
898+
.realm = undefined,
899+
};
801900
return buf_len;
802901
}
803-
transfer._forbidden = false;
902+
transfer._auth_challenge = null;
804903

805904
transfer.bytes_received = buf_len;
806905
return buf_len;
807906
}
808907

809-
if (transfer._redirecting == false and transfer._forbidden == false) {
908+
if (transfer._redirecting == false and transfer._auth_challenge != null) {
810909
transfer.bytes_received += buf_len;
811910
}
812911

813912
if (buf_len != 2) {
913+
if (transfer._auth_challenge != null) {
914+
// try to parse auth challenge.
915+
if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or
916+
std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate"))
917+
{
918+
const ac = AuthChallenge.parse(
919+
transfer._auth_challenge.?.status,
920+
header,
921+
) catch |err| {
922+
// We can't parse the auth challenge
923+
log.err(.http, "parse auth challenge", .{ .err = err, .header = header });
924+
// Should we cancel the request? I don't think so.
925+
return buf_len;
926+
};
927+
transfer._auth_challenge = ac;
928+
}
929+
}
814930
return buf_len;
815931
}
816932

@@ -838,7 +954,7 @@ pub const Transfer = struct {
838954
return c.CURL_WRITEFUNC_ERROR;
839955
};
840956

841-
if (transfer._redirecting) {
957+
if (transfer._redirecting or transfer._auth_challenge != null) {
842958
return chunk_len;
843959
}
844960

0 commit comments

Comments
 (0)