-
Notifications
You must be signed in to change notification settings - Fork 5.5k
common: support parsing cookies into a map #17811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
d5b4539
ba00daa
4abd5cb
b38e16d
75186e0
fb5f668
ef434aa
61f8a7d
0cd3459
6e1641a
ba2e2cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -254,46 +254,69 @@ 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<bool (const absl::string_view&, const absl::string_view&)> 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. | ||||||
| // https://tools.ietf.org/html/rfc6265#section-4.1.1 | ||||||
| if (v.size() >= 2 && v.back() == '"' && v[0] == '"') { | ||||||
| v = v.substr(1, v.size() - 2); | ||||||
| } | ||||||
| return v; | ||||||
|
|
||||||
| bool continue_iteration = cookie_consumer(k, v); | ||||||
| if (!continue_iteration) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
then you don't need
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, moved the invocation inside if condition itself |
||||||
| 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] (const absl::string_view& k, const absl::string_view& v) -> bool { | ||||||
| if (key == k && !v.empty()) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this new code implements different behavior and theoretically may break certain setups. Consider the case of The old I think it makes sense to add a test for this case and make sure it passes for both versions.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yes, this new version is not equivalent to the old one. I'll try to make both the variants (single-cookie and map) compatible with the old behaviour.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a related note it appears that even the old behaviours haven't been consistent over time. For example if we use this new test as a reference: It fails before #17560 landed (changed header iter. from reverse to natural order) But passes on main:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Behaviour on main (and the older version before it got changed) is a little inconsistent because it:
And this ^ makes parsing out duplicate cookies spilt between multiple cookie headers fun.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any suggestions on how to proceed here ?
I'm inclined towards approaches 2 or 3 because it keeps the parsing simple in the long run - but this can break some setups. RFC 6265 - HTTP State Management Mechanism itself is pretty ambiguous about duplicate cookies and ordering:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm... That's a good catch! If the spec explicitly states that the order is unimportant then just pick the first value irrespective of its emptiness for the sake of perf. A second reviewer may give a second opinion on it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to take the first value for perf.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, switched to picking up the first occurrence of a cookie! Added tests so that this behavioural quirk has coverage. |
||||||
| value = std::string{v}; | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return EMPTY_STRING; | ||||||
| // continue iterating until a cookie that matches `key` is found. | ||||||
| return true; | ||||||
| }); | ||||||
|
|
||||||
| return value; | ||||||
| } | ||||||
|
|
||||||
| std::map<std::string, std::string> Utility::parseCookies(const RequestHeaderMap& headers) { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Switched to using For my curiosity: Why should it be preferred over
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Algorithmic complexity of inserts and lookups is logarithmic for |
||||||
| std::map<std::string, std::string> cookies; | ||||||
|
|
||||||
| forEachCookie(headers, Http::Headers::get().Cookie, [&cookies] (const absl::string_view& k, const absl::string_view& v) -> bool { | ||||||
| cookies.emplace(std::string{k}, std::string{v}); | ||||||
|
|
||||||
| // continue iterating until all cookies are processed. | ||||||
| return true; | ||||||
| }); | ||||||
|
|
||||||
| return cookies; | ||||||
| } | ||||||
|
|
||||||
| bool Utility::Url::initialize(absl::string_view absolute_url, bool is_connect) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,9 +138,17 @@ 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); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation parses all cookies into a map even when we just need a subset. Should
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if possible performance gain can balance increased code complexity. I'd rather keep it as it is for now.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah that instantiates many small strings, I think this should pass a lambda like the following to forEachCookies:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This may require consumers of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about this variant of parseCookies that accepts a key filter lambda ? It can support both static checks like these ^ OR something more dynamic like checking set / map membership. An overload can be added that that defaults
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the latter sounds good to me if we care about uniform handling of duplicate cookies. Checking for set membership may be even faster than the static string comparisons.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. Its being used like this in the oauth2 filter now: Kept static comparisons inside the predicate thinking that serial |
||
|
|
||
| const auto expires_it = cookies.find("OauthExpires"); | ||
| expires_ = expires_it != cookies.end() ? expires_it->second : EMPTY_STRING; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace it with a function defined in the anonymous namespace and call the function also for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, added |
||
|
|
||
| const auto token_it = cookies.find("BearerToken"); | ||
| token_ = token_it != cookies.end() ? token_it->second : EMPTY_STRING; | ||
|
|
||
| const auto hmac_it = cookies.find("OauthHMAC"); | ||
| hmac_ = hmac_it != cookies.end() ? hmac_it->second : EMPTY_STRING; | ||
|
|
||
| host_ = headers.Host()->value().getStringView(); | ||
|
|
||
| secret_.assign(secret.begin(), secret.end()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
absl::string_viewshould be passed by value as the spec suggests. No need to addconstand&.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out! I have dropped
constand&as recommended in the spec