From fb439bda2e3674c3d02a5e723d8436cb5765896d Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 21:38:40 -0400 Subject: [PATCH] Split authentication challenges with split_unquoted split_challenge_segments() did what split_unquoted() does, trimming and dropping empty segments included, so parse_www_authenticate() uses the latter. --- httplib.h | 120 ++++++++++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 71 deletions(-) diff --git a/httplib.h b/httplib.h index 8628cf70..0768d87c 100644 --- a/httplib.h +++ b/httplib.h @@ -10334,41 +10334,6 @@ public: static WSInit wsinit_; #endif -// RFC 9110 Section 11.6.1 defines a challenge list as -// WWW-Authenticate = #challenge -// challenge = auth-scheme [ 1*SP ( token68 / [ #auth-param ] ) ] -// auth-param = token BWS "=" BWS ( token / quoted-string ) -// so a server may offer several schemes, each with its own comma-separated -// auth-param list, in either order and either as separate field lines or -// packed into one. Splitting on every comma would break apart a challenge's -// own param list; splitting only on the first space would miss a Digest -// challenge that isn't first. Split on commas that aren't inside a -// quoted-string instead, then track which scheme each resulting segment -// belongs to: a segment whose text before "=" contains whitespace (or that -// has no "=" at all) starts a new challenge named by its leading token. -inline std::vector split_challenge_segments(const std::string &s) { - std::vector segments; - size_t start = 0; - auto in_quotes = false; - for (size_t i = 0; i < s.size(); i++) { - auto c = s[i]; - if (in_quotes) { - if (c == '\\' && i + 1 < s.size()) { - i++; - } else if (c == '"') { - in_quotes = false; - } - } else if (c == '"') { - in_quotes = true; - } else if (c == ',') { - segments.push_back(s.substr(start, i - start)); - start = i + 1; - } - } - segments.push_back(s.substr(start)); - return segments; -} - inline std::string unescape_quoted_pairs(const std::string &s) { std::string out; out.reserve(s.size()); @@ -10395,6 +10360,19 @@ inline std::string escape_quoted_pairs(const std::string &s) { return out; } +// RFC 9110 Section 11.6.1 defines a challenge list as +// WWW-Authenticate = #challenge +// challenge = auth-scheme [ 1*SP ( token68 / [ #auth-param ] ) ] +// auth-param = token BWS "=" BWS ( token / quoted-string ) +// so a server may offer several schemes, each with its own comma-separated +// auth-param list, in either order and either as separate field lines or +// packed into one. Splitting on every comma would break apart a challenge's +// own param list; splitting only on the first space would miss a Digest +// challenge that isn't first. Split on commas that aren't inside a +// quoted-string instead (split_unquoted), then track which scheme each +// resulting segment belongs to: a segment whose text before "=" contains +// whitespace (or that has no "=" at all) starts a new challenge named by its +// leading token. inline bool parse_www_authenticate(const Response &res, std::map &auth, bool is_proxy) { @@ -10404,44 +10382,44 @@ inline bool parse_www_authenticate(const Response &res, auto found_digest = false; auto in_digest_challenge = false; - for (const auto &raw_segment : split_challenge_segments(combined)) { - auto segment = trim_copy(raw_segment); - if (segment.empty()) { continue; } + split_unquoted( + combined.data(), combined.data() + combined.size(), ',', + [&](const char *b, const char *e) { + std::string segment(b, e); + auto eq_pos = segment.find('='); + // BWS is allowed on both sides of "=", so the text naming the key (or, + // for the first segment of a challenge, " ") must be + // trimmed before its boundaries are inspected. + auto key_part = trim_copy( + eq_pos == std::string::npos ? segment : segment.substr(0, eq_pos)); + auto space_pos = key_part.find_last_of(" \t"); + if (space_pos != std::string::npos || eq_pos == std::string::npos) { + // "[ ]" starts a new challenge. + auto scheme_end = + space_pos == std::string::npos ? key_part.size() : space_pos; + // RFC 7616 Section 3.7: a server may offer more than one Digest + // challenge (e.g. SHA-256 and MD5); keep only the first so a nonce + // from one challenge is never paired with another's algorithm. + in_digest_challenge = + !found_digest && + case_ignore::equal(key_part.substr(0, scheme_end), "Digest"); + if (in_digest_challenge) { found_digest = true; } + if (space_pos == std::string::npos) { + // Bare scheme (or a token68), no auth-param on this segment. + return; + } + key_part = key_part.substr(space_pos + 1); + } - auto eq_pos = segment.find('='); - // BWS is allowed on both sides of "=", so the text naming the key (or, - // for the first segment of a challenge, " ") must be - // trimmed before its boundaries are inspected. - auto key_part = trim_copy( - eq_pos == std::string::npos ? segment : segment.substr(0, eq_pos)); - auto space_pos = key_part.find_last_of(" \t"); - if (space_pos != std::string::npos || eq_pos == std::string::npos) { - // "[ ]" starts a new challenge. - auto scheme_end = - space_pos == std::string::npos ? key_part.size() : space_pos; - // RFC 7616 Section 3.7: a server may offer more than one Digest - // challenge (e.g. SHA-256 and MD5); keep only the first so a nonce - // from one challenge is never paired with another's algorithm. - in_digest_challenge = - !found_digest && - case_ignore::equal(key_part.substr(0, scheme_end), "Digest"); - if (in_digest_challenge) { found_digest = true; } - if (space_pos == std::string::npos) { - // Bare scheme (or a token68), no auth-param on this segment. - continue; - } - key_part = key_part.substr(space_pos + 1); - } + if (!in_digest_challenge) { return; } - if (!in_digest_challenge) { continue; } - - auto val = trim_copy(segment.substr(eq_pos + 1)); - auto unquoted = trim_double_quotes_copy(val); - if (unquoted.size() != val.size()) { - unquoted = unescape_quoted_pairs(unquoted); - } - auth[std::move(key_part)] = std::move(unquoted); - } + auto val = trim_copy(segment.substr(eq_pos + 1)); + auto unquoted = trim_double_quotes_copy(val); + if (unquoted.size() != val.size()) { + unquoted = unescape_quoted_pairs(unquoted); + } + auth[std::move(key_part)] = std::move(unquoted); + }); // RFC 7616 Section 3.3 requires realm and nonce on every Digest challenge; // make_digest_authentication_header() dereferences both unconditionally, so