Skip to content
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

Copy http headers to ThreadContext strictly #45945

Merged
merged 10 commits into from Oct 30, 2019

Conversation

jkakavas
Copy link
Member

Previous behavior while copying HTTP headers to the ThreadContext,
would allow multiple HTTP headers with the same name, handling only
the first occurrence and disregarding the rest of the values. This
can be confusing when dealing with multiple Headers as it is not
obvious which value is read and which ones are silently dropped.

According to RFC-7230, a client must not send multiple header fields
with the same field name in a HTTP message, unless the entire field
value for this header is defined as a comma separated list or this
specific header is a well-known exception.

This commits changes the behavior in order to be more compliant to
the aforementioned RFC by requiring the classes that implement
ActionPlugin to declare if a header can be multi-valued or not when
registering this header to be copied over to the ThreadContext in
ActionPlugin#getRestHeaders.
If the header is allowed to be multivalued, then all such headers
are read from the HTTP request and their values get concatenated in
a comma-separated string.
If the header is not allowed to be multivalued, and the HTTP
request contains multiple such Headers with different values, the
request is rejected with a 400 status.

Previous behavior while copying HTTP headers to the ThreadContext,
would allow multiple HTTP headers with the same name, handling only
the first occurence and disregarding the rest of the values. This
can be confusing when dealing with multiple Headers as it is not
obvious which value is read and which ones are silently dropped.

According to RFC-7230, a client must not send multiple header fields
with the same field name in a HTTP message, unless the entire field
value for this header is defined as a comma separated list or this
specific header is a well-known exception.

This commits changes the behavior in order to be more compliant to
the aforementioned RFC by requiring the classes that implement
ActionPlugin to declare if a header can be multi-valued or not when
registering this header to be copied over to the ThreadContext in
ActionPlugin#getRestHeaders.
If the header is allowed to be multivalued, then all such headers
are read from the HTTP request and their values get concatenated in
a comma-separated string.
If the header is not allowed to be multivalued, and the HTTP
request contains multiple such Headers with different values, the
request is rejected with a 400 status.
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-core-infra

@jkakavas
Copy link
Member Author

For 7.x, we can refrain from making any breaking changes and minimize the changes required by only introducing RestRequest#getSingleValuedHeader and replacing RestRequest#header in https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/rest/RestController.java#L259.

This would make the assumption that all registered headers are meant to be single-valued, but I think it is a fair assumption for all currently registered headers in all plugins, since we would only read the first of the values either way.

@jasontedor jasontedor self-requested a review August 25, 2019 19:12
@jkakavas
Copy link
Member Author

@elasticmachine run elasticsearch-ci/1

@jkakavas
Copy link
Member Author

jkakavas commented Sep 4, 2019

@jasontedor I dont mean to rush things, just doing a round on my open PRs.

}
} catch (IllegalStateException e){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} catch (IllegalStateException e){
} catch (IllegalStateException e) {

public Collection<RestHeaderDefinition> getRestHeaders() {
return Arrays.asList(
new RestHeaderDefinition(CustomRealm.USER_HEADER, true),
new RestHeaderDefinition(CustomRealm.PW_HEADER, true));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these be false ?
I know it doesn't matter for testing purposes, but we point people to this as an example, and I think we want the default expaction for security examples to be single-valued headers.

@jkakavas
Copy link
Member Author

@jasontedor ping for a review 🙏

Copy link
Member

@jasontedor jasontedor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkakavas I'm so sorry I lost track of this one, it's unacceptable.

Overall the approach looks good, I left a comment for consideration.

}
} catch (IllegalStateException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's too bad we have to use an unchecked exception for flow control here. Can we look for another way? For example, one suggestion would be to pull the multi-valued validation into this method instead of holding it in RestRequest#getSingleValuedHeader.

Or, if this is not palatable, or if we can't find another way, can RestRequest#getSingleValuedHeader at least declare this exception in its Javadocs?

Copy link
Member

@jasontedor jasontedor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, I left one more observation, and a minor nit.

createSimpleErrorResponse(channel, BAD_REQUEST, "multiple values for single-valued header [" + name + "]."));
return;
} else {
threadContext.putHeader(name, headerValues.stream().distinct().collect(Collectors.joining(",")));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is effectively going to collect allocate a set to collect the header values twice in case multi-value headers are not allowed. Once to collect the header values into a set to see if its size is one, and then again (when the set is size one) to do the trivial join. Since we always need to collect the distinct values (either to check if we should fail the request, or put the header in the context), I think we should refactor this slightly to avoid collecting twice and all the allocations that come with it.

String httpHeader = request.header(key);
if (httpHeader != null) {
threadContext.putHeader(key, httpHeader);
for (RestHeaderDefinition restHeader : headersToCopy) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop iterator can be final.

Copy link
Member

@jasontedor jasontedor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That looks great @jkakavas. Thanks for the iterations.

@jkakavas
Copy link
Member Author

@elasticmachine run elasticsearch-ci/packaging-sample-matrix

@jkakavas
Copy link
Member Author

packaging sample was actually successful : https://elasticsearch-ci.elastic.co/job/elastic+elasticsearch+pull-request+packaging-tests-sample/8348/

BUILD SUCCESSFUL in 1h 28m 56s
--
339 actionable tasks: 321 executed, 17 from cache, 1 up-to-date

@jkakavas
Copy link
Member Author

@elasticmachine run elasticsearch-ci/packaging-sample

@jkakavas
Copy link
Member Author

17:34:54 OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f8371dd0000, 65536, 1) failed; error='Cannot allocate memory' (errno=12)
17:34:54 #
17:34:54 # There is insufficient memory for the Java Runtime Environment to continue.
17:34:54 # Native memory allocation (mmap) failed to map 65536 bytes for committing reserved memory.
17:34:54 # An error report file with more information is saved as:
17:34:54 # /var/lib/jenkins/workspace/elastic+elasticsearch+pull-request+packaging-tests-sample/hs_err_pid2493731.log

:(

@jkakavas
Copy link
Member Author

@elasticmachine run elasticsearch-ci/packaging-sample

@jkakavas jkakavas merged commit 1fc1df9 into elastic:master Oct 30, 2019
jkakavas added a commit to jkakavas/elasticsearch that referenced this pull request Oct 30, 2019
Previous behavior while copying HTTP headers to the ThreadContext,
would allow multiple HTTP headers with the same name, handling only
the first occurrence and disregarding the rest of the values. This
can be confusing when dealing with multiple Headers as it is not
obvious which value is read and which ones are silently dropped.

According to RFC-7230, a client must not send multiple header fields
with the same field name in a HTTP message, unless the entire field
value for this header is defined as a comma separated list or this
specific header is a well-known exception.

This commits changes the behavior in order to be more compliant to
the aforementioned RFC by assuming all HTTP headers are single
valued and throwing an error if a header with many values is sent.

Backport of elastic#45945 without the breaking change in
ActionPlugin#getRestHeaders
jkakavas added a commit to jkakavas/elasticsearch that referenced this pull request Oct 31, 2019
Previous behavior while copying HTTP headers to the ThreadContext,
would allow multiple HTTP headers with the same name, handling only
the first occurrence and disregarding the rest of the values. This
can be confusing when dealing with multiple Headers as it is not
obvious which value is read and which ones are silently dropped.

According to RFC-7230, a client must not send multiple header fields
with the same field name in a HTTP message, unless the entire field
value for this header is defined as a comma separated list or this
specific header is a well-known exception.

This commits changes the behavior in order to be more compliant to
the aforementioned RFC by requiring the classes that implement
ActionPlugin to declare if a header can be multi-valued or not when
registering this header to be copied over to the ThreadContext in
ActionPlugin#getRestHeaders.
If the header is allowed to be multivalued, then all such headers
are read from the HTTP request and their values get concatenated in
a comma-separated string.
If the header is not allowed to be multivalued, and the HTTP
request contains multiple such Headers with different values, the
request is rejected with a 400 status.
jkakavas added a commit that referenced this pull request Oct 31, 2019
Previous behavior while copying HTTP headers to the ThreadContext,
would allow multiple HTTP headers with the same name, handling only
the first occurrence and disregarding the rest of the values. This
can be confusing when dealing with multiple Headers as it is not
obvious which value is read and which ones are silently dropped.

According to RFC-7230, a client must not send multiple header fields
with the same field name in a HTTP message, unless the entire field
value for this header is defined as a comma separated list or this
specific header is a well-known exception.

This commits changes the behavior in order to be more compliant to
the aforementioned RFC by requiring the classes that implement
ActionPlugin to declare if a header can be multi-valued or not when
registering this header to be copied over to the ThreadContext in
ActionPlugin#getRestHeaders.
If the header is allowed to be multivalued, then all such headers
are read from the HTTP request and their values get concatenated in
a comma-separated string.
If the header is not allowed to be multivalued, and the HTTP
request contains multiple such Headers with different values, the
request is rejected with a 400 status.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants