core: carry credentials through the probe and its auth handshake

libcurl with CURLAUTH_ANY answers a 401/407 by resending the request with
an Authorization header. Two spots in net/ cut that short:

- http_client's header callback delivered the response head exactly once
  and latched `head_delivered`, so after an auth challenge the caller only
  ever saw the 401 — never the 2xx of the authenticated resend. Reset the
  latch when a fresh status line follows a delivered 401/407 (redirects
  never reach that path — their head is suppressed).

- the prober's head callbacks return DataAction::abort to skip the body,
  which also aborts the transfer mid-handshake. Return `proceed` for a
  401/407 when credentials were supplied, so curl's resend can run; the
  real status lands on the next header block.

Also give ProbeRequest an `auth` field (default scheme == none) and pass
it through base_request(), so a re-probe after a 401 can present the
credentials the user just entered. No behaviour change when no auth is
configured.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
This commit is contained in:
2026-09-10 20:19:05 +04:00
co-authored by Claude Sonnet 5
parent 479f882324
commit afaded85f8
3 changed files with 22 additions and 2 deletions
+1
View File
@@ -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
+7
View File
@@ -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;
+14 -2
View File
@@ -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<TransferStats> 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> &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<TransferStats> r) { on_range_done(p, std::move(r)); };