|
|
@@ -9580,38 +9580,105 @@ 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<std::string> split_challenge_segments(const std::string &s) {
|
|
|
+ std::vector<std::string> 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());
|
|
|
+ for (size_t i = 0; i < s.size(); i++) {
|
|
|
+ if (s[i] == '\\' && i + 1 < s.size()) {
|
|
|
+ out += s[++i];
|
|
|
+ } else {
|
|
|
+ out += s[i];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
inline bool parse_www_authenticate(const Response &res,
|
|
|
std::map<std::string, std::string> &auth,
|
|
|
bool is_proxy) {
|
|
|
auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate";
|
|
|
- if (res.has_header(auth_key)) {
|
|
|
- thread_local auto re =
|
|
|
- std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~");
|
|
|
- auto s = res.get_header_value(auth_key);
|
|
|
- auto pos = s.find(' ');
|
|
|
- if (pos != std::string::npos) {
|
|
|
- auto type = s.substr(0, pos);
|
|
|
- if (type == "Basic") {
|
|
|
- return false;
|
|
|
- } else if (type == "Digest") {
|
|
|
- s = s.substr(pos + 1);
|
|
|
- auto beg = std::sregex_iterator(s.begin(), s.end(), re);
|
|
|
- for (auto i = beg; i != std::sregex_iterator(); ++i) {
|
|
|
- const auto &m = *i;
|
|
|
- auto key = s.substr(static_cast<size_t>(m.position(1)),
|
|
|
- static_cast<size_t>(m.length(1)));
|
|
|
- auto val = m.length(2) > 0
|
|
|
- ? s.substr(static_cast<size_t>(m.position(2)),
|
|
|
- static_cast<size_t>(m.length(2)))
|
|
|
- : s.substr(static_cast<size_t>(m.position(3)),
|
|
|
- static_cast<size_t>(m.length(3)));
|
|
|
- auth[std::move(key)] = std::move(val);
|
|
|
- }
|
|
|
- return true;
|
|
|
+ auto combined = get_combined_header_value(res.headers, auth_key);
|
|
|
+ if (combined.empty()) { return false; }
|
|
|
+
|
|
|
+ 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; }
|
|
|
+
|
|
|
+ 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, "<scheme> <key>") 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) {
|
|
|
+ // "<scheme>[ <key>]" 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) { 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);
|
|
|
}
|
|
|
- return false;
|
|
|
+
|
|
|
+ // A challenge with no auth-param can't produce a usable Authorization
|
|
|
+ // header, so treat it the same as no Digest challenge at all.
|
|
|
+ return found_digest && !auth.empty();
|
|
|
}
|
|
|
|
|
|
class ContentProviderAdapter {
|