-
Notifications
You must be signed in to change notification settings - Fork 5.5k
ext_proc: Honor HeaderValueOption.append_action, keep append option as well for backward compatibility #29862
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 34 commits
5b8758f
16e39bd
cce7b89
37185a7
44b5297
cf742c2
98c852f
00acddf
cc4a88f
c6372f3
ba085b3
32d6589
f70f495
872aaa3
fbd6de8
214929f
ad337c2
f8ce857
a54590f
b8bc861
98cd440
77ac18f
c19cc3d
aa1cb9b
aca53c2
962dd86
43b3982
23e2fb8
291817c
061f3aa
ad19ed9
fec6ae3
65d38d4
e994879
a30229b
121a42b
7ae3f78
3cc6e3f
f7c4aac
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 |
|---|---|---|
|
|
@@ -24,6 +24,9 @@ using Stats::Counter; | |
| using envoy::service::ext_proc::v3::BodyMutation; | ||
| using envoy::service::ext_proc::v3::HeaderMutation; | ||
|
|
||
| using HeaderAppendAction = envoy::config::core::v3::HeaderValueOption::HeaderAppendAction; | ||
| using HeaderValueOption = envoy::config::core::v3::HeaderValueOption; | ||
|
|
||
| bool MutationUtils::headerInMatcher( | ||
| absl::string_view key, const std::vector<Matchers::StringMatcherPtr>& header_matchers) { | ||
| return std::any_of(header_matchers.begin(), header_matchers.end(), | ||
|
|
@@ -146,6 +149,11 @@ absl::Status MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, | |
| } | ||
| } | ||
|
|
||
| bool append_mode_for_append_action; | ||
| Filters::Common::MutationRules::CheckOperation check_op_for_append_action; | ||
| Filters::Common::MutationRules::CheckResult checkResult_for_append_action; | ||
| auto is_duplicate = false; | ||
|
|
||
| for (const auto& sh : mutation.set_headers()) { | ||
| if (!sh.has_header()) { | ||
| continue; | ||
|
|
@@ -174,40 +182,131 @@ absl::Status MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, | |
| return absl::InvalidArgumentError("Invalid character in set_headers mutation."); | ||
| } | ||
| const LowerCaseString header_name(sh.header().key()); | ||
| const bool append = PROTOBUF_GET_WRAPPED_OR_DEFAULT(sh, append, false); | ||
| const auto check_op = (append && !headers.get(header_name).empty()) ? CheckOperation::APPEND | ||
| : CheckOperation::SET; | ||
| auto check_result = checker.check(check_op, header_name, header_value); | ||
| if (replacing_message && header_name == Http::Headers::get().Method) { | ||
| // Special handling to allow changing ":method" when the | ||
| // CONTINUE_AND_REPLACE option is selected, to stay compatible. | ||
| check_result = CheckResult::OK; | ||
| } | ||
| switch (check_result) { | ||
| case CheckResult::OK: | ||
| ENVOY_LOG(trace, "Setting header {} append = {}", sh.header().key(), append); | ||
| if (append) { | ||
| headers.addCopy(header_name, header_value); | ||
| } else { | ||
| headers.setCopy(header_name, header_value); | ||
|
|
||
| if (sh.append_action().has_value()) { | ||
| switch (sh.append_action()) { | ||
| case HeaderValueOption::APPEND_IF_EXISTS_OR_ADD: | ||
| // Check if the header already exists with the same name and value. | ||
| is_duplicate = false; | ||
|
Contributor
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 think the desired behavior is that If ext_proc server sends duplicate headers with identical key/value, ext_proc filter probably should just accept them all in the case: APPEND_IF_EXISTS_OR_ADD. Is there any harm for Envoy to have duplicate headers when forwarding the message to upstream/downstream?
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 feel it should be the other way around, as stated in RFC 7230 section 3.2.2: A sender MUST NOT generate multiple header fields with the same field name in a message unless either the entire field value for that header field is defined as a comma-separated list [i.e., #(values)] or the header field is a well-known exception (as noted below). We probably should be compliant with RFC with exceptions that are allowed(e.g., set-cookie is well-known exception)
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. Thank you all for your great comments! I can update the code accordingly. Can we have the requirement or specification documented somewhere so that I exactly follow it?
Contributor
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. Adding @yanavlasov What does UHV do if client sends multiple identical headers with same value, like
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 think ext_proc should follow the Envoy's general principle and existing implementation that is compliant with RFC I mentioned above: Please note:
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. Thanks @htuch ! @Ronnie-personal. As I mentioned above. we should just use addCopy to handle @yanjunxiang-google Regarding your previous question about rejecting malformed mutation. I feel ext_proc doesn't need to reject malformed headers as long as low-level codec reject them. And it will be ext_proc server's responsibility of using right API. For example, single-value header like
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.
If I understand correctly, 'append' should be deprecated and we need to honor 'append_action'.
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. In theory, Therefore, I think
WDYT?
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. Thanks for the detailed instructions. It makes perfect sense. I have one more question if you don't mind. FYI, I have also tried 'if (sh.append_action().has_value())', the code errored out.
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.
|
||
| if (!headers.get(header_name).empty()) { | ||
| Http::HeaderMap::GetResult result = headers.get(header_name); | ||
| absl::string_view existing_value; | ||
| for (size_t i = 0; i < result.size(); ++i) { | ||
| existing_value = result[i]->value().getStringView(); | ||
|
|
||
| // Compare the existing value with your desired header_value | ||
| if (existing_value == header_value) { | ||
| is_duplicate = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (!is_duplicate) { | ||
| append_mode_for_append_action = true; | ||
| check_op_for_append_action = | ||
| (!headers.get(header_name).empty()) ? CheckOperation::APPEND : CheckOperation::SET; | ||
| checkResult_for_append_action = handleCheckResult( | ||
| headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, | ||
| header_name, header_value, append_mode_for_append_action); | ||
| if (checkResult_for_append_action == CheckResult::FAIL) { | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| } | ||
| break; | ||
| case HeaderValueOption::ADD_IF_ABSENT: | ||
| if (headers.get(header_name).empty()) { | ||
| append_mode_for_append_action = true; | ||
| check_op_for_append_action = CheckOperation::SET; | ||
| checkResult_for_append_action = handleCheckResult( | ||
| headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, | ||
| header_name, header_value, append_mode_for_append_action); | ||
| if (checkResult_for_append_action == CheckResult::FAIL) { | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| } | ||
| break; | ||
| case HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD: | ||
| append_mode_for_append_action = false; | ||
| check_op_for_append_action = CheckOperation::SET; | ||
| checkResult_for_append_action = handleCheckResult( | ||
| headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, | ||
| header_name, header_value, append_mode_for_append_action); | ||
| if (checkResult_for_append_action == CheckResult::FAIL) { | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| break; | ||
| case HeaderValueOption::OVERWRITE_IF_EXISTS: | ||
| if (!headers.get(header_name).empty()) { | ||
| append_mode_for_append_action = false; | ||
| check_op_for_append_action = CheckOperation::SET; | ||
| checkResult_for_append_action = handleCheckResult( | ||
| headers, replacing_message, checker, rejected_mutations, check_op_for_append_action, | ||
| header_name, header_value, append_mode_for_append_action); | ||
| if (checkResult_for_append_action == CheckResult::FAIL) { | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| } | ||
| break; | ||
| default: | ||
| // Handle unknown/invalid append_action value | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid append_action value ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| } else { | ||
| const bool append_mode = PROTOBUF_GET_WRAPPED_OR_DEFAULT(sh, append, false); | ||
| const auto check_op = (append_mode && !headers.get(header_name).empty()) | ||
| ? CheckOperation::APPEND | ||
| : CheckOperation::SET; | ||
| auto checkResult = handleCheckResult(headers, replacing_message, checker, rejected_mutations, | ||
| check_op, header_name, header_value, append_mode); | ||
| if (checkResult == CheckResult::FAIL) { | ||
| return absl::InvalidArgumentError(absl::StrCat( | ||
| "Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| break; | ||
| case CheckResult::IGNORE: | ||
| ENVOY_LOG(debug, "Header {} may not be modified per rules", header_name); | ||
| rejected_mutations.inc(); | ||
| break; | ||
| case CheckResult::FAIL: | ||
| ENVOY_LOG(debug, "Header {} may not be modified. Returning error", header_name); | ||
| rejected_mutations.inc(); | ||
| return absl::InvalidArgumentError( | ||
| absl::StrCat("Invalid attempt to modify ", static_cast<absl::string_view>(header_name))); | ||
| } | ||
| } | ||
|
|
||
| // After header mutation, check the ending headers are not exceeding the HCM limit. | ||
| return headerMutationResultCheck(headers, rejected_mutations); | ||
| } | ||
|
|
||
| // Define a common function to handle the check result logic | ||
| Filters::Common::MutationRules::CheckResult MutationUtils::handleCheckResult( | ||
| Http::HeaderMap& headers, bool replacing_message, | ||
| const Filters::Common::MutationRules::Checker& checker, Stats::Counter& rejected_mutations, | ||
| Filters::Common::MutationRules::CheckOperation check_op, Http::LowerCaseString header_name, | ||
| absl::string_view header_value, bool append_mode) { | ||
| auto check_result = checker.check(check_op, header_name, header_value); | ||
| if (replacing_message && header_name == Http::Headers::get().Method) { | ||
| // Special handling to allow changing ":method" when the | ||
| // CONTINUE_AND_REPLACE option is selected, to stay compatible. | ||
| check_result = CheckResult::OK; | ||
| } | ||
| // Print the value as an integer | ||
| switch (check_result) { | ||
| case CheckResult::OK: | ||
| if (append_mode) { | ||
| headers.addCopy(header_name, header_value); | ||
| } else { | ||
| headers.setCopy(header_name, header_value); | ||
| } | ||
| break; | ||
| case CheckResult::IGNORE: | ||
| rejected_mutations.inc(); | ||
| break; | ||
| case CheckResult::FAIL: | ||
| rejected_mutations.inc(); | ||
| // You can add additional handling for the FAIL case here if needed. | ||
| break; | ||
| } | ||
|
|
||
| return check_result; | ||
| } | ||
|
|
||
| void MutationUtils::applyBodyMutations(const BodyMutation& mutation, Buffer::Instance& buffer) { | ||
| switch (mutation.mutation_case()) { | ||
| case BodyMutation::MutationCase::kClearBody: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
Let's revert this change in API proto. I didn't mean to change the API here (Sorry if my previous comment in the other thread misleads you)
Basically we don't need to impose additional restriction on it and it can just follow the general rule in header map.
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.
No problem at all, I will revert it back.