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
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,31 @@ class HeaderString {
*/
bool find(const char* str) const { return strstr(c_str(), str); }

/**
* HeaderString is in token list form, each token separated by commas or whitespace,
* see https://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.1 for more information,
* header field value's case sensitivity depends on each header.
* @return whether contains token in case insensitive manner.
*/
bool caseInsensitiveContains(const char* token) const {
// 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.

Do we have a test case for this branch?

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.

}

// Find token substring, skip if it's partial of other token.
const char* tokens = c_str();
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 ((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.

return true;
}
}

return false;
}

/**
* Set the value of the string by copying data into it. This overwrites any existing string.
*/
Expand Down
22 changes: 17 additions & 5 deletions source/common/http/conn_manager_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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?

request_headers.removeEnvoyInternalRequest();
request_headers.removeKeepAlive();
request_headers.removeProxyConnection();
request_headers.removeTransferEncoding();

// 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)) {
// 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.
const 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.

}
} else {
request_headers.removeConnection();
request_headers.removeUpgrade();
}

request_headers.removeEnvoyInternalRequest();
request_headers.removeKeepAlive();
request_headers.removeProxyConnection();
request_headers.removeTransferEncoding();

// If we are "using remote address" this means that we create/append to XFF with our immediate
// peer. Cases where we don't "use remote address" include trusted double proxy where we expect
// our peer to have already properly set XFF, etc.
Expand Down
7 changes: 4 additions & 3 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,11 @@ bool Utility::isInternalRequest(const HeaderMap& headers) {
}

bool Utility::isWebSocketUpgradeRequest(const HeaderMap& headers) {
// In firefox the "Connection" request header value is "keep-alive, Upgrade",
// we should check if it contains the "Upgrade" token.
return (headers.Connection() && headers.Upgrade() &&
(0 == StringUtil::caseInsensitiveCompare(
headers.Connection()->value().c_str(),
Http::Headers::get().ConnectionValues.Upgrade.c_str())) &&
headers.Connection()->value().caseInsensitiveContains(
Http::Headers::get().ConnectionValues.Upgrade.c_str()) &&
(0 == StringUtil::caseInsensitiveCompare(
headers.Upgrade()->value().c_str(),
Http::Headers::get().UpgradeValues.WebSocket.c_str())));
Expand Down
16 changes: 16 additions & 0 deletions test/common/http/header_map_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,22 @@ TEST(HeaderStringTest, All) {
EXPECT_EQ(11U, string.size());
EXPECT_EQ(HeaderString::Type::Reference, string.type());
}

// caseInsensitiveContains
{
std::string static_string("keep-alive, Upgrade, close");
HeaderString string(static_string);
EXPECT_TRUE(string.caseInsensitiveContains("keep-alive"));
EXPECT_TRUE(string.caseInsensitiveContains("Keep-alive"));
EXPECT_TRUE(string.caseInsensitiveContains("Upgrade"));
EXPECT_TRUE(string.caseInsensitiveContains("upgrade"));
EXPECT_TRUE(string.caseInsensitiveContains("close"));
EXPECT_TRUE(string.caseInsensitiveContains("Close"));
EXPECT_TRUE(string.caseInsensitiveContains(""));
EXPECT_FALSE(string.caseInsensitiveContains("keep"));
EXPECT_FALSE(string.caseInsensitiveContains("alive"));
EXPECT_FALSE(string.caseInsensitiveContains("grade"));
}
}

TEST(HeaderMapImplTest, InlineInsert) {
Expand Down
3 changes: 2 additions & 1 deletion test/integration/fake_upstream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,13 @@ FakeRawConnectionPtr FakeUpstream::waitForRawConnection() {
return connection;
}

void FakeRawConnection::waitForData(uint64_t num_bytes) {
std::string FakeRawConnection::waitForData(uint64_t num_bytes) {
std::unique_lock<std::mutex> lock(lock_);
while (data_.size() != num_bytes) {
ENVOY_LOG(debug, "waiting for {} bytes of data", num_bytes);
connection_event_.wait(lock);
}
return data_;
}

void FakeRawConnection::write(const std::string& data) {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/fake_upstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ class FakeRawConnection : Logger::Loggable<Logger::Id::testing>, public FakeConn
connection_.addReadFilter(Network::ReadFilterSharedPtr{new ReadFilter(*this)});
}

void waitForData(uint64_t num_bytes);
std::string waitForData(uint64_t num_bytes);
void write(const std::string& data);

private:
Expand Down
32 changes: 20 additions & 12 deletions test/integration/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -193,31 +193,35 @@ TEST_P(IntegrationTest, WebSocketConnectionDownstreamDisconnect) {
IntegrationTcpClientPtr tcp_client;
FakeRawConnectionPtr fake_upstream_connection;
const std::string upgrade_req_str = "GET /websocket/test HTTP/1.1\r\nHost: host\r\nConnection: "
"Upgrade\r\nUpgrade: websocket\r\n\r\n";
"keep-alive, Upgrade\r\nUpgrade: websocket\r\n\r\n";
const std::string upgrade_resp_str =
"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n";

tcp_client = makeTcpConnection(lookupPort("http"));
// Send websocket upgrade request
// The request path gets rewritten from /websocket/test to /websocket.
// The size of headers received by the destination is 225 bytes.
// The size of headers received by the destination is 228 bytes.
tcp_client->write(upgrade_req_str);
fake_upstream_connection = fake_upstreams_[0]->waitForRawConnection();
fake_upstream_connection->waitForData(225);
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

}
// Accept websocket upgrade request
fake_upstream_connection->write(upgrade_resp_str);
tcp_client->waitForData(upgrade_resp_str);
// Standard TCP proxy semantics post upgrade
tcp_client->write("hello");
// datalen = 225 + strlen(hello)
fake_upstream_connection->waitForData(230);
// datalen = 228 + strlen(hello)
fake_upstream_connection->waitForData(233);
fake_upstream_connection->write("world");
tcp_client->waitForData(upgrade_resp_str + "world");
tcp_client->write("bye!");
// downstream disconnect
tcp_client->close();
// datalen = 225 + strlen(hello) + strlen(bye!)
fake_upstream_connection->waitForData(234);
// datalen = 228 + strlen(hello) + strlen(bye!)
fake_upstream_connection->waitForData(237);
fake_upstream_connection->waitForDisconnect();
}

Expand All @@ -230,23 +234,27 @@ TEST_P(IntegrationTest, WebSocketConnectionUpstreamDisconnect) {
IntegrationTcpClientPtr tcp_client;
FakeRawConnectionPtr fake_upstream_connection;
const std::string upgrade_req_str = "GET /websocket/test HTTP/1.1\r\nHost: host\r\nConnection: "
"Upgrade\r\nUpgrade: websocket\r\n\r\n";
"keep-alive, Upgrade\r\nUpgrade: websocket\r\n\r\n";
const std::string upgrade_resp_str =
"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n";
tcp_client = makeTcpConnection(lookupPort("http"));
// Send websocket upgrade request
tcp_client->write(upgrade_req_str);
fake_upstream_connection = fake_upstreams_[0]->waitForRawConnection();
// The request path gets rewritten from /websocket/test to /websocket.
// The size of headers received by the destination is 225 bytes.
fake_upstream_connection->waitForData(225);
// The size of headers received by the destination is 228 bytes.
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"));
}
// Accept websocket upgrade request
fake_upstream_connection->write(upgrade_resp_str);
tcp_client->waitForData(upgrade_resp_str);
// Standard TCP proxy semantics post upgrade
tcp_client->write("hello");
// datalen = 225 + strlen(hello)
fake_upstream_connection->waitForData(230);
// datalen = 228 + strlen(hello)
fake_upstream_connection->waitForData(233);
fake_upstream_connection->write("world");
// upstream disconnect
fake_upstream_connection->close();
Expand Down