diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 07d24106e1f15..9a803b9d8e906 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -254,20 +254,25 @@ bool maybeAdjustForIpv6(absl::string_view absolute_url, uint64_t& offset, uint64 return true; } -absl::string_view parseCookie(absl::string_view cookie_value, absl::string_view key) { - // Split the cookie header into individual cookies. - for (const auto& s : StringUtil::splitToken(cookie_value, ";")) { - // Find the key part of the cookie (i.e. the name of the cookie). - size_t first_non_space = s.find_first_not_of(' '); - size_t equals_index = s.find('='); - if (equals_index == absl::string_view::npos) { - // The cookie is malformed if it does not have an `=`. Continue - // checking other cookies in this header. - continue; - } - absl::string_view k = s.substr(first_non_space, equals_index - first_non_space); - // If the key matches, parse the value from the rest of the cookie string. - if (k == key) { +void forEachCookie( + const HeaderMap& headers, const LowerCaseString& cookie_header, + const std::function& cookie_consumer) { + const Http::HeaderMap::GetResult cookie_headers = headers.get(cookie_header); + + for (size_t index = 0; index < cookie_headers.size(); index++) { + auto cookie_header_value = cookie_headers[index]->value().getStringView(); + + // Split the cookie header into individual cookies. + for (const auto& s : StringUtil::splitToken(cookie_header_value, ";")) { + // Find the key part of the cookie (i.e. the name of the cookie). + size_t first_non_space = s.find_first_not_of(' '); + size_t equals_index = s.find('='); + if (equals_index == absl::string_view::npos) { + // The cookie is malformed if it does not have an `=`. Continue + // checking other cookies in this header. + continue; + } + absl::string_view k = s.substr(first_non_space, equals_index - first_non_space); absl::string_view v = s.substr(equals_index + 1, s.size() - 1); // Cookie values may be wrapped in double quotes. @@ -275,25 +280,53 @@ absl::string_view parseCookie(absl::string_view cookie_value, absl::string_view if (v.size() >= 2 && v.back() == '"' && v[0] == '"') { v = v.substr(1, v.size() - 2); } - return v; + + if (!cookie_consumer(k, v)) { + return; + } } } - return EMPTY_STRING; } std::string parseCookie(const HeaderMap& headers, const std::string& key, const LowerCaseString& cookie) { - const Http::HeaderMap::GetResult cookie_headers = headers.get(cookie); + std::string value; - for (size_t index = 0; index < cookie_headers.size(); index++) { - auto cookie_header_value = cookie_headers[index]->value().getStringView(); - absl::string_view result = parseCookie(cookie_header_value, key); - if (!result.empty()) { - return std::string{result}; + // Iterate over each cookie & return if its value is not empty. + forEachCookie(headers, cookie, [&key, &value](absl::string_view k, absl::string_view v) -> bool { + if (key == k) { + value = std::string{v}; + return false; } - } - return EMPTY_STRING; + // continue iterating until a cookie that matches `key` is found. + return true; + }); + + return value; +} + +absl::flat_hash_map +Utility::parseCookies(const RequestHeaderMap& headers) { + return Utility::parseCookies(headers, [](absl::string_view) -> bool { return true; }); +} + +absl::flat_hash_map +Utility::parseCookies(const RequestHeaderMap& headers, + const std::function& key_filter) { + absl::flat_hash_map cookies; + + forEachCookie(headers, Http::Headers::get().Cookie, + [&cookies, &key_filter](absl::string_view k, absl::string_view v) -> bool { + if (key_filter(k)) { + cookies.emplace(k, v); + } + + // continue iterating until all cookies are processed. + return true; + }); + + return cookies; } bool Utility::Url::initialize(absl::string_view absolute_url, bool is_connect) { diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 7929e687e5802..d2ba67613c35d 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -255,6 +255,23 @@ std::string stripQueryString(const HeaderString& path); **/ std::string parseCookieValue(const HeaderMap& headers, const std::string& key); +/** + * Parse cookies from header into a map. + * @param headers supplies the headers to get cookies from. + * @param key_filter predicate that returns true for every cookie key to be included. + * @return absl::flat_hash_map cookie map. + **/ +absl::flat_hash_map +parseCookies(const RequestHeaderMap& headers, + const std::function& key_filter); + +/** + * Parse cookies from header into a map. + * @param headers supplies the headers to get cookies from. + * @return absl::flat_hash_map cookie map. + **/ +absl::flat_hash_map parseCookies(const RequestHeaderMap& headers); + /** * Parse a particular value out of a set-cookie * @param headers supplies the headers to get the set-cookie from. diff --git a/source/extensions/filters/http/oauth2/filter.cc b/source/extensions/filters/http/oauth2/filter.cc index 8a42db24cd90e..d0234dd733b09 100644 --- a/source/extensions/filters/http/oauth2/filter.cc +++ b/source/extensions/filters/http/oauth2/filter.cc @@ -107,6 +107,12 @@ std::string encodeResourceList(const Protobuf::RepeatedPtrField& re void setBearerToken(Http::RequestHeaderMap& headers, const std::string& token) { headers.setInline(authorization_handle.handle(), absl::StrCat("Bearer ", token)); } + +std::string findValue(const absl::flat_hash_map& map, + const std::string& key) { + const auto value_it = map.find(key); + return value_it != map.end() ? value_it->second : EMPTY_STRING; +} } // namespace FilterConfig::FilterConfig( @@ -138,9 +144,13 @@ FilterStats FilterConfig::generateStats(const std::string& prefix, Stats::Scope& void OAuth2CookieValidator::setParams(const Http::RequestHeaderMap& headers, const std::string& secret) { - expires_ = Http::Utility::parseCookieValue(headers, "OauthExpires"); - token_ = Http::Utility::parseCookieValue(headers, "BearerToken"); - hmac_ = Http::Utility::parseCookieValue(headers, "OauthHMAC"); + const auto& cookies = Http::Utility::parseCookies(headers, [](absl::string_view key) -> bool { + return key == "OauthExpires" || key == "BearerToken" || key == "OauthHMAC"; + }); + + expires_ = findValue(cookies, "OauthExpires"); + token_ = findValue(cookies, "BearerToken"); + hmac_ = findValue(cookies, "OauthHMAC"); host_ = headers.Host()->value().getStringView(); secret_.assign(secret.begin(), secret.end()); diff --git a/test/common/http/utility_test.cc b/test/common/http/utility_test.cc index 76304a34bfb40..25d86b680ac6b 100644 --- a/test/common/http/utility_test.cc +++ b/test/common/http/utility_test.cc @@ -549,6 +549,16 @@ TEST(HttpUtility, TestParseCookie) { EXPECT_EQ(value, "abc123"); } +TEST(HttpUtility, TestParseCookieDuplicates) { + TestRequestHeaderMapImpl headers{{"someheader", "10.0.0.1"}, + {"cookie", "a=; b=1; a=2"}, + {"cookie", "a=3; b=2"}, + {"cookie", "b=3"}}; + + EXPECT_EQ(Utility::parseCookieValue(headers, "a"), ""); + EXPECT_EQ(Utility::parseCookieValue(headers, "b"), "1"); +} + TEST(HttpUtility, TestParseSetCookie) { TestRequestHeaderMapImpl headers{ {"someheader", "10.0.0.1"}, @@ -598,6 +608,33 @@ TEST(HttpUtility, TestParseCookieWithQuotes) { EXPECT_EQ(Utility::parseCookieValue(headers, "leadingdquote"), "\"foobar"); } +TEST(HttpUtility, TestParseCookies) { + TestRequestHeaderMapImpl headers{ + {"someheader", "10.0.0.1"}, + {"cookie", "dquote=\"; quoteddquote=\"\"\""}, + {"cookie", "leadingdquote=\"foobar;"}, + {"cookie", "abc=def; token=\"abc123\"; Expires=Wed, 09 Jun 2021 10:18:14 GMT"}}; + + const auto& cookies = Utility::parseCookies(headers); + + EXPECT_EQ(cookies.at("token"), "abc123"); + EXPECT_EQ(cookies.at("dquote"), "\""); + EXPECT_EQ(cookies.at("quoteddquote"), "\""); + EXPECT_EQ(cookies.at("leadingdquote"), "\"foobar"); +} + +TEST(HttpUtility, TestParseCookiesDuplicates) { + TestRequestHeaderMapImpl headers{{"someheader", "10.0.0.1"}, + {"cookie", "a=; b=1; a=2"}, + {"cookie", "a=3; b=2"}, + {"cookie", "b=3"}}; + + const auto& cookies = Utility::parseCookies(headers); + + EXPECT_EQ(cookies.at("a"), ""); + EXPECT_EQ(cookies.at("b"), "1"); +} + TEST(HttpUtility, TestParseSetCookieWithQuotes) { TestRequestHeaderMapImpl headers{ {"someheader", "10.0.0.1"},