@@ -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+
577639pub 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