diff --git a/core/include/vdm/net/probe.hpp b/core/include/vdm/net/probe.hpp index 2122190..5b1e425 100644 --- a/core/include/vdm/net/probe.hpp +++ b/core/include/vdm/net/probe.hpp @@ -35,6 +35,7 @@ struct ProbeRequest { std::string user_agent; std::string referrer; ProxyConfig proxy; + AuthConfig auth; // credentials for a re-probe after a 401 (leave scheme == none otherwise) long connect_timeout_ms = 15000; long overall_timeout_ms = 25000; // download.probe deadline is 30 s diff --git a/core/src/net/http_client.cpp b/core/src/net/http_client.cpp index 25f9575..a83b206 100644 --- a/core/src/net/http_client.cpp +++ b/core/src/net/http_client.cpp @@ -195,6 +195,13 @@ struct HttpClient::Impl { std::string_view line(buf, total); if (line.starts_with("HTTP/")) { + // A new status line after we already delivered a 401/407 means libcurl's + // CURLAUTH_ANY handshake just resent with credentials: let the head of this + // second response be delivered too, so callers see the real (2xx/4xx) status + // rather than the challenge. Redirects never reach here delivered — their head + // is suppressed below — so this only fires for the auth resend. + if (st->head_delivered && (st->line_status == 401 || st->line_status == 407)) + st->head_delivered = false; st->line_status = status_from_line(line); st->head.headers.clear(); // keep only the final response's headers return total; diff --git a/core/src/net/probe.cpp b/core/src/net/probe.cpp index 608e02e..0a2c2f2 100644 --- a/core/src/net/probe.cpp +++ b/core/src/net/probe.cpp @@ -134,6 +134,7 @@ struct Prober::Impl { r.user_agent = pr.user_agent; r.referrer = pr.referrer; r.proxy = pr.proxy; + r.auth = pr.auth; r.follow_redirects = true; r.accept_encoding = false; r.connect_timeout_ms = pr.connect_timeout_ms; @@ -155,7 +156,7 @@ struct Prober::Impl { TransferCallbacks cbs; cbs.on_head = [p](const ResponseHead &h) { absorb_head(p->result, h); - return DataAction::abort; + return head_action(p, h); }; cbs.on_data = [](ConstByteSpan) { return DataAction::abort; }; cbs.on_finished = [p](Result r) { on_head_done(p, std::move(r)); }; @@ -165,6 +166,17 @@ struct Prober::Impl { client_.start(std::move(req), std::move(cbs)); } + // We want no body from a probe, so the head callback normally aborts after headers. + // The exception: a 401/407 when we were handed credentials — libcurl's CURLAUTH_ANY + // has to see that response before it resends with Authorization, so let this one + // through (a HEAD has no body; the ranged GET's is a single byte). The final status + // then lands on the next header block. + static DataAction head_action(const std::shared_ptr

&p, const ResponseHead &h) { + if ((h.status == 401 || h.status == 407) && p->job.req.auth.scheme != AuthScheme::none) + return DataAction::proceed; + return DataAction::abort; + } + static void absorb_head(ProbeResult &res, const ResponseHead &h) { if (h.status) res.http_status = h.status; @@ -242,7 +254,7 @@ struct Prober::Impl { TransferCallbacks cbs; cbs.on_head = [p](const ResponseHead &h) { absorb_range_head(p->result, h); - return DataAction::abort; // we don't need the one body byte + return head_action(p, h); // abort after headers, except a 401/407 with creds }; cbs.on_data = [](ConstByteSpan) { return DataAction::abort; }; cbs.on_finished = [p](Result r) { on_range_done(p, std::move(r)); };