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

websocket: http request header Connection may contains multiple values #2070

Merged
merged 6 commits into from
Nov 21, 2017
Merged

websocket: http request header Connection may contains multiple values #2070

merged 6 commits into from
Nov 21, 2017

Conversation

tangxinfa
Copy link
Contributor

http request header Connection may contains multiple values, such as: keep-alive, Upgrade

Description:
Don't use "strcasecmp" to do strict match, but use "strcasestr" to do partial match, this fix the 400 error response in firefox, because the Connection and Upgrade request header removed by envoy, become a invalid Websocket request.

@mattklein123
Copy link
Member

@tangxinfa thank you for the fix! We will need DCO, format fix, and some tests. Please let us know if you are willing to do those things. cc @rshriram

(0 == StringUtil::caseInsensitiveCompare(
headers.Connection()->value().c_str(),
Http::Headers::get().ConnectionValues.Upgrade.c_str())) &&
(NULL != strcasestr(headers.Connection()->value().c_str(), Http::Headers::get().ConnectionValues.Upgrade.c_str())) &&
Copy link
Member

Choose a reason for hiding this comment

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

Don't we still want this to be case insensitive? Maybe we should also do a split operation on ,, it would be more robust, at the expense of performance.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking the same thing about splitting on comma. I was looking for a helper/utility that would do this, and I don't see one (which surprised me).

Perhaps we can add a method to HeaderString for bool entryContains(const LowerCaseString& str) const;

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, do you think the perf cost will be too high to split it and return it? It would be faster to search in place, but the interface would be less-nice and less-reusable.

Copy link
Member

Choose a reason for hiding this comment

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

I agree that in general we shouldn't copy and split on the hot path when serving reqs. In this particular case, it might be OK to do this perhaps, since we already know we're most likely doing an upgrade due to the earlier test for the Upgrade: header, and if we're doing a Websockets upgrade, it's probably fine to pay a tiny bit extra for what is presumably a long lived session ¯_(ツ)_/¯

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with that. If we profile and find we're spending too much time there, we can fix it then.

Copy link
Member

Choose a reason for hiding this comment

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

yeah in this case because we are already guarded by the existence of the upgrade header I think it's fine to split after that fact.

Copy link
Member

Choose a reason for hiding this comment

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

Strcasestr is case insensitive. The question here is whether to abstract this into stringutil or not. This particular case, given that the contributor is doing a one off contribution, I am reluctant to insist on him debugging more :). I can do the follow up and clean it up as it’s not bad as is. But I leave it to you guys to decide on this.

@rshriram
Copy link
Member

Thank you for testing, tracking down the issue and fixing this. Would you mind copy pasting the explanation in the PR into a comment above the if block, so that others would understand why substring search is being used?

With regard to the tests, tweak one of the websocket integration tests in test/integration/ and change the connection upgrade to a keepalive,upgrade. This will test if any other piece in Envoy breaks (none should as we use tcp upstream).

firefox's Connection header value is: keep-alive, Upgrade

Signed-off-by: tangxinfa <[email protected]>
Copy link
Member

@rshriram rshriram left a comment

Choose a reason for hiding this comment

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

Lgtm except for missing comment about why token compare is needed.

headers.Connection()->value().caseInsensitiveContains(
Http::Headers::get().ConnectionValues.Upgrade.c_str()) &&
headers.Upgrade()->value().caseInsensitiveContains(
Http::Headers::get().UpgradeValues.WebSocket.c_str()));
Copy link
Member

Choose a reason for hiding this comment

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

Nit: please copy paste the explanation you had for doing the token comparison (about Firefox sending multiple tokens in the connection string)

request_headers.removeTransferEncoding();
if (contentLength0) {
request_headers.insertContentLength().value(uint64_t(0));
}
Copy link
Member

Choose a reason for hiding this comment

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

This seems unrelated to this PR. What is the rationale for this?

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Generally LGTM. Thanks a lot for fixing this. A few small comments.

bool caseInsensitiveContains(const char* token) const {
const int n = strlen(token);
if (n == 0) {
return true;
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test case for this branch?

// after transfer-encoding striped here, the upstream request become invalid:
// chunked encoding with no request body.
// We can fix it by explicitly add a "content-length: 0" request header here.
bool no_body = (!request_headers.TransferEncoding() && !request_headers.ContentLength());
Copy link
Member

Choose a reason for hiding this comment

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

nit: const bool

// We can fix it by explicitly add a "content-length: 0" request header here.
bool no_body = (!request_headers.TransferEncoding() && !request_headers.ContentLength());
if (no_body) {
request_headers.insertContentLength().value(uint64_t(0));
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test case where we check for the existence of content-length: 0? We should be able to look for this in one of the conn_manager_impl websocket tests.

const char* tokens = c_str();
const char* separators = " ,";
for (const char* p = tokens; (p = strcasestr(p, token)); p += n) {
if ((p == tokens || strchr(separators, *(p - 1)) != NULL) &&
Copy link
Member

Choose a reason for hiding this comment

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

nit: s/NULL/nullptr in both places.

}
const char* tokens = c_str();
const char* separators = " ,";
for (const char* p = tokens; (p = strcasestr(p, token)); p += n) {
Copy link
Member

Choose a reason for hiding this comment

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

this logic is really hard to quickly read. Can you add a small comment on what it is doing?


// If this is a WebSocket Upgrade request, do not remove the Connection and Upgrade headers,
// as we forward them verbatim to the upstream hosts.
if (protocol != Protocol::Http11 || !Utility::isWebSocketUpgradeRequest(request_headers)) {
if (protocol == Protocol::Http11 && Utility::isWebSocketUpgradeRequest(request_headers)) {
// Current WebSocket implementation re-use the HTTP1 codec to send upgrade headers to
Copy link
Member

Choose a reason for hiding this comment

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

Grammar nits, please just replace with (reflow as needed to 100 col):

// The current WebSocket implementation re-uses the HTTP1 codec to send upgrade headers to
// the upstream host. This adds the "transfer-encoding: chunked" request header if the stream 
// has not ended and content-length does not exist. In HTTP1.1, if transfer-encoding and 
// content-length both do not exist this means there is no request body. After transfer-encoding 
// is stripped here, the upstream request becomes invalid. We can fix it by explicitly adding a 
// "content-length: 0" request header here.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thank you, a few more comments.

const char* tokens = c_str();
for (const char* p = tokens; (p = strcasestr(p, token)); p += n) {
if ((p == tokens || *(p - 1) == ' ' || *(p - 1) == ',') &&
(*(p + n) == '\0' || *(p + n) == ' ' || *(p + n) == ',')) {
Copy link
Member

Choose a reason for hiding this comment

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

Don't you need to check here that the length of the remaining header is >= n before you do this? I think this code can overrun the header. Please make sure you add more test cases to cover small headers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it advance the pointer p only if found the token, there is no overrun. I will add more test cases cover small headers.

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see. Sorry got it. Yup you are right. Please just add some small header tests and we are good.

// Avoid dead loop if token argument is empty.
const int n = strlen(token);
if (n == 0) {
return true;
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this should return false?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, you are right.

const std::string data = fake_upstream_connection->waitForData(228);
// The chunked transfer-encoding for no body upstream request must valid
if (std::string::npos != data.find("transfer-encoding: chunked")) {
EXPECT_NE(std::string::npos, data.find("\r\n\r\n0\r\n"));
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 not really clear what this is testing for, can you search for the entire "content-length" etc. header? Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

mattklein123
mattklein123 previously approved these changes Nov 20, 2017
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Awesome. thank you.

@@ -22,18 +22,30 @@ void ConnectionManagerUtility::mutateRequestHeaders(
Runtime::RandomGenerator& random, Runtime::Loader& runtime,
const LocalInfo::LocalInfo& local_info) {
// Clean proxy headers.
Copy link
Member

Choose a reason for hiding this comment

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

oops sorry, one more nit: Can you please move this comment down with the rest of header removes?

Signed-off-by: tangxinfa <[email protected]>
@mattklein123 mattklein123 merged commit 3e6e4a7 into envoyproxy:master Nov 21, 2017
@tangxinfa tangxinfa deleted the fix-isWebSocketUpgradeRequest branch November 21, 2017 02:33
Shikugawa pushed a commit to Shikugawa/envoy that referenced this pull request Mar 28, 2020
* Authenticate an exchanged token

* Change issuer name and jwt-authn output with key being original issuer

* Revised the code based on the discussion

* Address review comments and add a test

* Address new review comments

* Add integration tests and address review comments

* Fix a flaky test and address new review comments

* Small grammar fixes

* Revise the function of finding the token header

* Use case-insensitive compare for the header name

* Change the name of a variable

* Revise log statements
jpsim pushed a commit that referenced this pull request Nov 28, 2022
Description: Allow tests to avoid TLS Cert Verification
- With this PR, only one `UpstreamTlsContext` supports the configurable `trust_chain_verification`:  [`&base_tls_h2_socket`](https://github.com/envoyproxy/envoy-mobile/blob/v0.4.5.02082022/library/common/config/config.cc#L112). The other one, `&base_tls_socket`, does not.

- `&base_tls_h2_socket` has been inlined. Consequently, `&base_tls_h2_socket` does not exist anymore. Unfortunately, when a config variable is assigned in the Java code, it won't get substituted when encountered inside another config variable.

- Swift/objective-c/c++ configuration classes were not updated. Only Java and Kotlin were.

Risk Level: Moderate
Testing: New test and CI
Docs Changes: N/A
Release Notes: N/A
Signed-off-by: Charles Le Borgne <[email protected]>
Signed-off-by: JP Simard <[email protected]>
jpsim pushed a commit that referenced this pull request Nov 29, 2022
Description: Allow tests to avoid TLS Cert Verification
- With this PR, only one `UpstreamTlsContext` supports the configurable `trust_chain_verification`:  [`&base_tls_h2_socket`](https://github.com/envoyproxy/envoy-mobile/blob/v0.4.5.02082022/library/common/config/config.cc#L112). The other one, `&base_tls_socket`, does not.

- `&base_tls_h2_socket` has been inlined. Consequently, `&base_tls_h2_socket` does not exist anymore. Unfortunately, when a config variable is assigned in the Java code, it won't get substituted when encountered inside another config variable.

- Swift/objective-c/c++ configuration classes were not updated. Only Java and Kotlin were.

Risk Level: Moderate
Testing: New test and CI
Docs Changes: N/A
Release Notes: N/A
Signed-off-by: Charles Le Borgne <[email protected]>
Signed-off-by: JP Simard <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants