Skip to content

Commit f8acde6

Browse files
committed
wip
1 parent 7869159 commit f8acde6

File tree

5 files changed

+215
-13
lines changed

5 files changed

+215
-13
lines changed

src/cdp/cdp.zig

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

480-
pub fn fetchEnable(self: *Self) !void {
480+
pub fn fetchEnable(self: *Self, authRequests: bool) !void {
481481
try self.cdp.browser.notification.register(.http_request_intercept, self, onHttpRequestIntercept);
482+
if (authRequests) {
483+
try self.cdp.browser.notification.register(.http_request_auth_required, self, onHttpRequestAuthRequired);
484+
}
482485
}
483486

484487
pub fn fetchDisable(self: *Self) void {
485488
self.cdp.browser.notification.unregister(.http_request_intercept, self);
489+
self.cdp.browser.notification.unregister(.http_request_auth_required, self);
486490
}
487491

488492
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
@@ -548,6 +552,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
548552
try gop.value_ptr.appendSlice(arena, try arena.dupe(u8, msg.data));
549553
}
550554

555+
pub fn onHttpRequestAuthRequired(ctx: *anyopaque, data: *const Notification.RequestAuthRequired) !void {
556+
const self: *Self = @alignCast(@ptrCast(ctx));
557+
defer self.resetNotificationArena();
558+
try @import("domains/fetch.zig").requestAuthRequired(self.notification_arena, self, data);
559+
}
560+
551561
fn resetNotificationArena(self: *Self) void {
552562
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
553563
}

src/cdp/domains/fetch.zig

Lines changed: 97 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,56 @@ fn continueRequest(cmd: anytype) !void {
276274
return cmd.sendResult(null, .{});
277275
}
278276

277+
fn continueWithAuth(cmd: anytype) !void {
278+
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
279+
const params = (try cmd.params(struct {
280+
requestId: []const u8, // "INTERCEPT-{d}"
281+
authChallengeResponse: struct {
282+
response: []const u8,
283+
username: ?[]const u8 = null,
284+
password: ?[]const u8 = null,
285+
},
286+
})) orelse return error.InvalidParams;
287+
288+
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
289+
290+
var intercept_state = &bc.intercept_state;
291+
const request_id = try idFromRequestId(params.requestId);
292+
const transfer = intercept_state.remove(request_id) orelse return error.RequestNotFound;
293+
294+
log.debug(.cdp, "request intercept", .{
295+
.state = "continue with auth",
296+
.id = transfer.id,
297+
.response = params.authChallengeResponse.response,
298+
});
299+
300+
if (!std.mem.eql(u8, params.authChallengeResponse.response, "ProvideCredentials")) {
301+
transfer.abortAuthChallenge();
302+
return cmd.sendResult(null, .{});
303+
}
304+
305+
// cancel the request, deinit the transfer on error.
306+
errdefer transfer.abortAuthChallenge();
307+
308+
const username = params.authChallengeResponse.username orelse "";
309+
const password = params.authChallengeResponse.password orelse "";
310+
311+
// restart the request with the provided credentials.
312+
// we need to duplicate the cre
313+
const arena = transfer.arena.allocator();
314+
transfer.updateCredentials(
315+
try std.fmt.allocPrintZ(arena, "{s}:{s}", .{ username, password }),
316+
);
317+
318+
try bc.cdp.browser.http_client.process(transfer);
319+
320+
if (intercept_state.empty()) {
321+
page.request_intercepted = false;
322+
}
323+
324+
return cmd.sendResult(null, .{});
325+
}
326+
279327
fn fulfillRequest(cmd: anytype) !void {
280328
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
281329

@@ -346,6 +394,50 @@ fn failRequest(cmd: anytype) !void {
346394
return cmd.sendResult(null, .{});
347395
}
348396

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

src/http/Client.zig

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
325325
}
326326

327327
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer));
328+
329+
// add credentials
330+
if (req.credentials) |creds| {
331+
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXYUSERPWD, creds.ptr));
332+
}
328333
}
329334

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

373+
// In case of auth challenge
374+
if (transfer._auth_challenge != null) {
375+
if (transfer.client.notification) |notification| {
376+
var wait_for_interception = false;
377+
notification.dispatch(.http_request_auth_required, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
378+
if (wait_for_interception) {
379+
// the request is put on hold to be intercepted.
380+
// In this case we ignore callbacks for now.
381+
// Note: we don't deinit transfer on purpose: we want to keep
382+
// using it for the following request.
383+
self.endTransfer(transfer);
384+
continue;
385+
}
386+
}
387+
}
388+
368389
// release it ASAP so that it's available; some done_callbacks
369390
// will load more resources.
370391
self.endTransfer(transfer);
371392

372393
defer transfer.deinit();
373394

374395
if (errorCheck(msg.data.result)) {
396+
375397
// In case of request w/o data, we need to call the header done
376398
// callback now.
377399
if (!transfer._header_done_called) {
@@ -542,6 +564,7 @@ pub const Request = struct {
542564
body: ?[]const u8 = null,
543565
cookie_jar: *CookieJar,
544566
resource_type: ResourceType,
567+
credentials: ?[:0]const u8 = null,
545568

546569
// arbitrary data that can be associated with this request
547570
ctx: *anyopaque = undefined,
@@ -559,6 +582,44 @@ pub const Request = struct {
559582
};
560583
};
561584

585+
pub const AuthChallenge = struct {
586+
source: enum { server, proxy },
587+
scheme: enum { basic, digest },
588+
realm: []const u8,
589+
590+
pub fn parse(header: []const u8) !AuthChallenge {
591+
var ac: AuthChallenge = .{
592+
.source = undefined,
593+
.realm = "TODO", // TODO parser and set realm
594+
.scheme = undefined,
595+
};
596+
597+
const sep = std.mem.indexOfPos(u8, header, 0, ": ") orelse return error.InvalidHeader;
598+
const hname = header[0..sep];
599+
const hvalue = header[sep + 2 ..];
600+
601+
if (std.ascii.eqlIgnoreCase("WWW-Authenticate", hname)) {
602+
ac.source = .server;
603+
} else if (std.ascii.eqlIgnoreCase("Proxy-Authenticate", hname)) {
604+
ac.source = .proxy;
605+
} else {
606+
return error.InvalidAuthChallenge;
607+
}
608+
609+
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, hvalue, std.ascii.whitespace[0..]), 0, " ") orelse hvalue.len;
610+
const _scheme = hvalue[0..pos];
611+
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
612+
ac.scheme = .basic;
613+
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
614+
ac.scheme = .digest;
615+
} else {
616+
return error.UnknownAuthChallengeScheme;
617+
}
618+
619+
return ac;
620+
}
621+
};
622+
562623
pub const Transfer = struct {
563624
arena: ArenaAllocator,
564625
id: usize = 0,
@@ -582,9 +643,9 @@ pub const Transfer = struct {
582643
_handle: ?*Handle = null,
583644

584645
_redirecting: bool = false,
585-
_forbidden: bool = false,
646+
_auth_challenge: ?AuthChallenge = null,
586647

587-
fn deinit(self: *Transfer) void {
648+
pub fn deinit(self: *Transfer) void {
588649
self.req.headers.deinit();
589650
if (self._handle) |handle| {
590651
self.client.handles.release(handle);
@@ -633,6 +694,10 @@ pub const Transfer = struct {
633694
self.req.url = url;
634695
}
635696

697+
pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void {
698+
self.req.credentials = userpwd;
699+
}
700+
636701
pub fn replaceRequestHeaders(self: *Transfer, allocator: Allocator, headers: []const Http.Header) !void {
637702
self.req.headers.deinit();
638703

@@ -657,6 +722,14 @@ pub const Transfer = struct {
657722
self.deinit();
658723
}
659724

725+
// abortAuthChallenge is called when an auth chanllenge interception is
726+
// abort. We don't call self.client.endTransfer here b/c it has been done
727+
// before interception process.
728+
pub fn abortAuthChallenge(self: *Transfer) void {
729+
self.client.requestFailed(self, error.AbortAuthChallenge);
730+
self.deinit();
731+
}
732+
660733
// redirectionCookies manages cookies during redirections handled by Curl.
661734
// It sets the cookies from the current response to the cookie jar.
662735
// It also immediately sets cookies for the following request.
@@ -782,20 +855,40 @@ pub const Transfer = struct {
782855
transfer._redirecting = false;
783856

784857
if (status == 401 or status == 407) {
785-
transfer._forbidden = true;
858+
// The auth challenge must be parsed from a following
859+
// WWW-Authenticate or Proxy-Authenticate header.
860+
transfer._auth_challenge = .{
861+
.source = undefined,
862+
.scheme = undefined,
863+
.realm = undefined,
864+
};
786865
return buf_len;
787866
}
788-
transfer._forbidden = false;
867+
transfer._auth_challenge = null;
789868

790869
transfer.bytes_received = buf_len;
791870
return buf_len;
792871
}
793872

794-
if (transfer._redirecting == false and transfer._forbidden == false) {
873+
if (transfer._redirecting == false and transfer._auth_challenge != null) {
795874
transfer.bytes_received += buf_len;
796875
}
797876

798877
if (buf_len != 2) {
878+
if (transfer._auth_challenge != null) {
879+
// try to parse auth challenge.
880+
if (std.ascii.startsWithIgnoreCase(header, "WWW-Authenticate") or
881+
std.ascii.startsWithIgnoreCase(header, "Proxy-Authenticate"))
882+
{
883+
const ac = AuthChallenge.parse(header) catch |err| {
884+
// We can't parse the auth challenge
885+
log.err(.http, "parse auth challenge", .{ .err = err, .header = header });
886+
// Should we cancel the request? I don't think so.
887+
return buf_len;
888+
};
889+
transfer._auth_challenge = ac;
890+
}
891+
}
799892
return buf_len;
800893
}
801894

@@ -823,7 +916,7 @@ pub const Transfer = struct {
823916
return c.CURL_WRITEFUNC_ERROR;
824917
};
825918

826-
if (transfer._redirecting) {
919+
if (transfer._redirecting or transfer._auth_challenge != null) {
827920
return chunk_len;
828921
}
829922

src/http/Http.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub const c = @cImport({
2222
@cInclude("curl/curl.h");
2323
});
2424

25-
pub const ENABLE_DEBUG = false;
25+
pub const ENABLE_DEBUG = true;
2626
pub const Client = @import("Client.zig");
2727
pub const Transfer = Client.Transfer;
2828

src/notification.zig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ pub const Notification = struct {
6464
http_request_start: List = .{},
6565
http_request_intercept: List = .{},
6666
http_request_done: List = .{},
67+
http_request_auth_required: List = .{},
6768
http_response_data: List = .{},
6869
http_response_header_done: List = .{},
6970
notification_created: List = .{},
@@ -77,6 +78,7 @@ pub const Notification = struct {
7778
http_request_fail: *const RequestFail,
7879
http_request_start: *const RequestStart,
7980
http_request_intercept: *const RequestIntercept,
81+
http_request_auth_required: *const RequestAuthRequired,
8082
http_request_done: *const RequestDone,
8183
http_response_data: *const ResponseData,
8284
http_response_header_done: *const ResponseHeaderDone,
@@ -106,6 +108,11 @@ pub const Notification = struct {
106108
wait_for_interception: *bool,
107109
};
108110

111+
pub const RequestAuthRequired = struct {
112+
transfer: *Transfer,
113+
wait_for_interception: *bool,
114+
};
115+
109116
pub const ResponseData = struct {
110117
data: []const u8,
111118
transfer: *Transfer,

0 commit comments

Comments
 (0)