Skip to content

Commit 58e923d

Browse files
committed
wip
1 parent 37d44e4 commit 58e923d

File tree

5 files changed

+225
-9
lines changed

5 files changed

+225
-9
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: 143 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,60 @@ 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,
284+
password: ?[]const u8,
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.abort(); // Is it the correct way to cancel the transfer?
302+
transfer.deinit();
303+
return cmd.sendResult(null, .{});
304+
}
305+
306+
// cancel the request, deinit the transfer on error.
307+
errdefer {
308+
transfer.abort(); // Is it the correct way to cancel the transfer?
309+
transfer.deinit();
310+
}
311+
312+
const username = params.authChallengeResponse.username orelse "";
313+
const password = params.authChallengeResponse.password orelse "";
314+
315+
// restart the request with the provided credentials.
316+
// we need to duplicate the cre
317+
const arena = transfer.arena.allocator();
318+
transfer.updateCredentials(
319+
try std.fmt.allocPrintZ(arena, "{s}:{s}", .{ username, password }),
320+
);
321+
322+
try bc.cdp.browser.http_client.process(transfer);
323+
324+
if (intercept_state.empty()) {
325+
page.request_intercepted = false;
326+
}
327+
328+
return cmd.sendResult(null, .{});
329+
}
330+
279331
fn fulfillRequest(cmd: anytype) !void {
280332
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
281333

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

401+
const AuthChallenge = struct {
402+
scheme: enum { basic, digest },
403+
realm: []const u8,
404+
405+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/WWW-Authenticate
406+
// Supports only basic and digest schemes.
407+
pub fn parse(header: []const u8) !AuthChallenge {
408+
var ac: AuthChallenge = .{
409+
.scheme = undefined,
410+
.realm = "",
411+
};
412+
413+
const pos = std.mem.indexOfPos(u8, std.mem.trim(u8, header, std.ascii.whitespace[0..]), 0, " ") orelse header.len;
414+
const _scheme = header[0..pos];
415+
if (std.ascii.eqlIgnoreCase(_scheme, "basic")) {
416+
ac.scheme = .basic;
417+
} else if (std.ascii.eqlIgnoreCase(_scheme, "digest")) {
418+
ac.scheme = .digest;
419+
} else {
420+
return error.UnknownAuthChallengeScheme;
421+
}
422+
423+
// TODO get the realm
424+
425+
return ac;
426+
}
427+
};
428+
429+
pub fn requestAuthRequired(arena: Allocator, bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {
430+
// unreachable because we _have_ to have a page.
431+
const session_id = bc.session_id orelse unreachable;
432+
const target_id = bc.target_id orelse unreachable;
433+
const page = bc.session.currentPage() orelse unreachable;
434+
435+
// We keep it around to wait for modifications to the request.
436+
// NOTE: we assume whomever created the request created it with a lifetime of the Page.
437+
// TODO: What to do when receiving replies for a previous page's requests?
438+
439+
const transfer = intercept.transfer;
440+
try bc.intercept_state.put(transfer);
441+
442+
var challenge: AuthChallenge = undefined;
443+
var source: enum { server, proxy } = undefined;
444+
var it = transfer.responseHeaderIterator();
445+
while (it.next()) |hdr| {
446+
if (std.ascii.eqlIgnoreCase("WWW-Authenticate", hdr.name)) {
447+
source = .server;
448+
challenge = try AuthChallenge.parse(hdr.value);
449+
break;
450+
}
451+
if (std.ascii.eqlIgnoreCase("Proxy-Authenticate", hdr.name)) {
452+
source = .proxy;
453+
challenge = try AuthChallenge.parse(hdr.value);
454+
break;
455+
}
456+
}
457+
458+
try bc.cdp.sendEvent("Fetch.authRequired", .{
459+
.requestId = try std.fmt.allocPrint(arena, "INTERCEPT-{d}", .{transfer.id}),
460+
.request = network.TransferAsRequestWriter.init(transfer),
461+
.frameId = target_id,
462+
.resourceType = switch (transfer.req.resource_type) {
463+
.script => "Script",
464+
.xhr => "XHR",
465+
.document => "Document",
466+
},
467+
.authChallenge = .{
468+
.source = if (source == .server) "Server" else "Proxy",
469+
.origin = "", // TODO get origin, could be the proxy address for example.
470+
.scheme = if (challenge.scheme == .digest) "digest" else "basic",
471+
.realm = challenge.realm,
472+
},
473+
.networkId = try std.fmt.allocPrint(arena, "REQ-{d}", .{transfer.id}),
474+
}, .{ .session_id = session_id });
475+
476+
log.debug(.cdp, "request auth required", .{
477+
.state = "paused",
478+
.id = transfer.id,
479+
.url = transfer.uri,
480+
});
481+
// Await continueWithAuth
482+
483+
intercept.wait_for_interception.* = true;
484+
page.request_intercepted = true;
485+
}
486+
349487
// Get u64 from requestId which is formatted as: "INTERCEPT-{d}"
350488
fn idFromRequestId(request_id: []const u8) !u64 {
351489
if (!std.mem.startsWith(u8, request_id, "INTERCEPT-")) {

src/http/Client.zig

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,11 @@ fn makeRequest(self: *Client, handle: *Handle, transfer: *Transfer) !void {
331331
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_COOKIE, cookies));
332332
}
333333

334+
// Proxy credentials
335+
if (req.credentials) |creds| {
336+
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PROXYUSERPWD, creds.ptr));
337+
}
338+
334339
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_PRIVATE, transfer));
335340
}
336341

@@ -372,10 +377,24 @@ fn perform(self: *Client, timeout_ms: c_int) !void {
372377
const easy = msg.easy_handle.?;
373378
const transfer = try Transfer.fromEasy(easy);
374379

380+
var code: c_long = undefined;
381+
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &code));
382+
std.debug.print("FAILED REQ: {any}\n", .{code});
383+
375384
// release it ASAP so that it's available; some done_callbacks
376385
// will load more resources.
377386
self.endTransfer(transfer);
378387

388+
// If the transfer is waiting for auth challenge interception, don't
389+
// deinit the transfer and don't call the callbacks. All will be done
390+
// later during the interception's response.
391+
// we only reset the transfer to release the handle and be reused later.
392+
if (transfer._auth_challenge) {
393+
log.debug(.http, "transfer reset", .{});
394+
transfer.reset();
395+
continue;
396+
}
397+
379398
defer transfer.deinit();
380399

381400
if (errorCheck(msg.data.result)) {
@@ -540,6 +559,7 @@ pub const Request = struct {
540559
body: ?[]const u8 = null,
541560
cookie_jar: *CookieJar,
542561
resource_type: ResourceType,
562+
credentials: ?[:0]const u8 = null,
543563

544564
// arbitrary data that can be associated with this request
545565
ctx: *anyopaque = undefined,
@@ -577,6 +597,9 @@ pub const Transfer = struct {
577597
_handle: ?*Handle = null,
578598

579599
_redirecting: bool = false,
600+
// True when a request returns a 401 or 407 and we wait for a request
601+
// interception to continue with auth.
602+
_auth_challenge: bool = false,
580603

581604
// use_proxy is set when the transfer has been associated to a given
582605
// connection in makeRequest().
@@ -585,7 +608,7 @@ pub const Transfer = struct {
585608
// stateful variables used to parse responses headers.
586609
_resp_header_status: enum { empty, first, next, end } = .empty,
587610

588-
fn deinit(self: *Transfer) void {
611+
pub fn deinit(self: *Transfer) void {
589612
self.req.headers.deinit();
590613
if (self._handle) |handle| {
591614
self.client.handles.release(handle);
@@ -594,6 +617,21 @@ pub const Transfer = struct {
594617
self.client.transfer_pool.destroy(self);
595618
}
596619

620+
fn reset(self: *Transfer) void {
621+
if (self._handle) |handle| {
622+
self.client.handles.release(handle);
623+
self._handle = null;
624+
}
625+
self.bytes_received = 0;
626+
self.proxy_response_header = null;
627+
self.response_header = null;
628+
self._notified_fail = false;
629+
self._redirecting = false;
630+
self._auth_challenge = false;
631+
self._use_proxy = false;
632+
self._resp_header_status = .empty;
633+
}
634+
597635
pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
598636
const req = self.req;
599637
return writer.print("{s} {s}", .{ @tagName(req.method), req.url });
@@ -609,6 +647,10 @@ pub const Transfer = struct {
609647
self._request_header_list = c.curl_slist_append(self._request_header_list, value);
610648
}
611649

650+
pub fn updateCredentials(self: *Transfer, userpwd: [:0]const u8) void {
651+
self.req.credentials = userpwd;
652+
}
653+
612654
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
613655
// for cookies
614656
self.uri = try std.Uri.parse(url);
@@ -796,6 +838,25 @@ pub const Transfer = struct {
796838
if (i >= ct.?.amount) break;
797839
}
798840

841+
// If the response requires an auth challenge and the auth
842+
// interception is enable, we must create a new request and wait
843+
// for the interception's response.
844+
// We won't call the req's callbacks now, but for the following request only.
845+
const status = transfer.response_header.?.status;
846+
if (status == 401 or status == 407) {
847+
log.debug(.http, "FORBIDDEN", .{});
848+
if (transfer.client.notification) |notification| {
849+
var wait_for_interception = false;
850+
notification.dispatch(.http_request_auth_required, &.{ .transfer = transfer, .wait_for_interception = &wait_for_interception });
851+
if (wait_for_interception) {
852+
log.debug(.http, "WAIT FOR INTERCEPT", .{});
853+
transfer._auth_challenge = true;
854+
// The user is send an invitation to intercept this request.
855+
return buf_len;
856+
}
857+
}
858+
}
859+
799860
transfer.req.header_callback(transfer) catch |err| {
800861
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
801862
// returning < buf_len terminates the request
@@ -824,7 +885,7 @@ pub const Transfer = struct {
824885
return c.CURL_WRITEFUNC_ERROR;
825886
};
826887

827-
if (transfer._redirecting) {
888+
if (transfer._redirecting or transfer._auth_challenge) {
828889
return chunk_len;
829890
}
830891

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)