// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/websockets/websocket_stream.h" #include #include #include #include #include "base/compiler_specific.h" #include "base/macros.h" #include "base/metrics/histogram.h" #include "base/metrics/histogram_samples.h" #include "base/metrics/statistics_recorder.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #include "base/timer/mock_timer.h" #include "base/timer/timer.h" #include "net/base/net_errors.h" #include "net/base/test_data_directory.h" #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/proxy/proxy_service.h" #include "net/socket/client_socket_handle.h" #include "net/socket/socket_test_util.h" #include "net/test/cert_test_util.h" #include "net/url_request/url_request_test_util.h" #include "net/websockets/websocket_basic_handshake_stream.h" #include "net/websockets/websocket_frame.h" #include "net/websockets/websocket_stream_create_test_base.h" #include "net/websockets/websocket_test_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" #include "url/origin.h" namespace net { namespace { // Simple builder for a SequencedSocketData object to save repetitive code. // It always sets the connect data to MockConnect(SYNCHRONOUS, OK), so it cannot // be used in tests where the connect fails. In practice, those tests never have // any read/write data and so can't benefit from it anyway. The arrays are not // copied. It is up to the caller to ensure they stay in scope until the test // ends. template scoped_ptr BuildSocketData( MockRead(&reads)[reads_count], MockWrite(&writes)[writes_count]) { scoped_ptr socket_data( new SequencedSocketData(reads, reads_count, writes, writes_count)); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); return socket_data; } // Builder for a SequencedSocketData that expects nothing. This does not // set the connect data, so the calling code must do that explicitly. scoped_ptr BuildNullSocketData() { return make_scoped_ptr(new SequencedSocketData(NULL, 0, NULL, 0)); } class MockWeakTimer : public base::MockTimer, public base::SupportsWeakPtr { public: MockWeakTimer(bool retain_user_task, bool is_repeating) : MockTimer(retain_user_task, is_repeating) {} }; static url::Origin LocalhostOrigin() { return url::Origin(GURL("http://localhost/")); } static url::Origin GoogleOrigin() { return url::Origin(GURL("http://google.com/")); } class WebSocketStreamCreateTest : public ::testing::Test, public WebSocketStreamCreateTestBase { public: ~WebSocketStreamCreateTest() override { // Permit any endpoint locks to be released. stream_request_.reset(); stream_.reset(); base::RunLoop().RunUntilIdle(); } void CreateAndConnectCustomResponse( const std::string& socket_url, const std::string& socket_host, const std::string& socket_path, const std::vector& sub_protocols, const url::Origin& origin, const std::string& extra_request_headers, const std::string& response_body, scoped_ptr timer = scoped_ptr()) { url_request_context_host_.SetExpectations( WebSocketStandardRequest(socket_path, socket_host, origin, extra_request_headers), response_body); CreateAndConnectStream(socket_url, sub_protocols, origin, std::move(timer)); } // |extra_request_headers| and |extra_response_headers| must end in "\r\n" or // errors like "Unable to perform synchronous IO while stopped" will occur. void CreateAndConnectStandard( const std::string& socket_url, const std::string& socket_host, const std::string& socket_path, const std::vector& sub_protocols, const url::Origin& origin, const std::string& extra_request_headers, const std::string& extra_response_headers, scoped_ptr timer = scoped_ptr()) { CreateAndConnectCustomResponse( socket_url, socket_host, socket_path, sub_protocols, origin, extra_request_headers, WebSocketStandardResponse(extra_response_headers), std::move(timer)); } void CreateAndConnectRawExpectations( const std::string& socket_url, const std::vector& sub_protocols, const url::Origin& origin, scoped_ptr socket_data, scoped_ptr timer = scoped_ptr()) { AddRawExpectations(std::move(socket_data)); CreateAndConnectStream(socket_url, sub_protocols, origin, std::move(timer)); } // Add additional raw expectations for sockets created before the final one. void AddRawExpectations(scoped_ptr socket_data) { url_request_context_host_.AddRawExpectations(std::move(socket_data)); } }; // There are enough tests of the Sec-WebSocket-Extensions header that they // deserve their own test fixture. class WebSocketStreamCreateExtensionTest : public WebSocketStreamCreateTest { public: // Performs a standard connect, with the value of the Sec-WebSocket-Extensions // header in the response set to |extensions_header_value|. Runs the event // loop to allow the connect to complete. void CreateAndConnectWithExtensions( const std::string& extensions_header_value) { CreateAndConnectStandard( "ws://localhost/testing_path", "localhost", "/testing_path", NoSubProtocols(), LocalhostOrigin(), "", "Sec-WebSocket-Extensions: " + extensions_header_value + "\r\n"); WaitUntilConnectDone(); } }; // Common code to construct expectations for authentication tests that receive // the auth challenge on one connection and then create a second connection to // send the authenticated request on. class CommonAuthTestHelper { public: CommonAuthTestHelper() : reads1_(), writes1_(), reads2_(), writes2_() {} scoped_ptr BuildSocketData1( const std::string& response) { request1_ = WebSocketStandardRequest("/", "localhost", LocalhostOrigin(), ""); writes1_[0] = MockWrite(SYNCHRONOUS, 0, request1_.c_str()); response1_ = response; reads1_[0] = MockRead(SYNCHRONOUS, 1, response1_.c_str()); reads1_[1] = MockRead(SYNCHRONOUS, OK, 2); // Close connection return BuildSocketData(reads1_, writes1_); } scoped_ptr BuildSocketData2( const std::string& request, const std::string& response) { request2_ = request; response2_ = response; writes2_[0] = MockWrite(SYNCHRONOUS, 0, request2_.c_str()); reads2_[0] = MockRead(SYNCHRONOUS, 1, response2_.c_str()); return BuildSocketData(reads2_, writes2_); } private: // These need to be object-scoped since they have to remain valid until all // socket operations in the test are complete. std::string request1_; std::string request2_; std::string response1_; std::string response2_; MockRead reads1_[2]; MockWrite writes1_[1]; MockRead reads2_[1]; MockWrite writes2_[1]; DISALLOW_COPY_AND_ASSIGN(CommonAuthTestHelper); }; // Data and methods for BasicAuth tests. class WebSocketStreamCreateBasicAuthTest : public WebSocketStreamCreateTest { protected: void CreateAndConnectAuthHandshake(const std::string& url, const std::string& base64_user_pass, const std::string& response2) { AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse)); static const char request2format[] = "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Pragma: no-cache\r\n" "Cache-Control: no-cache\r\n" "Authorization: Basic %s\r\n" "Upgrade: websocket\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "User-Agent:\r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: en-us,fr\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Extensions: permessage-deflate; " "client_max_window_bits\r\n" "\r\n"; const std::string request = base::StringPrintf(request2format, base64_user_pass.c_str()); CreateAndConnectRawExpectations( url, NoSubProtocols(), LocalhostOrigin(), helper_.BuildSocketData2(request, response2)); } static const char kUnauthorizedResponse[]; CommonAuthTestHelper helper_; }; class WebSocketStreamCreateDigestAuthTest : public WebSocketStreamCreateTest { protected: static const char kUnauthorizedResponse[]; static const char kAuthorizedRequest[]; CommonAuthTestHelper helper_; }; const char WebSocketStreamCreateBasicAuthTest::kUnauthorizedResponse[] = "HTTP/1.1 401 Unauthorized\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Basic realm=\"camelot\"\r\n" "\r\n"; // These negotiation values are borrowed from // http_auth_handler_digest_unittest.cc. Feel free to come up with new ones if // you are bored. Only the weakest (no qop) variants of Digest authentication // can be tested by this method, because the others involve random input. const char WebSocketStreamCreateDigestAuthTest::kUnauthorizedResponse[] = "HTTP/1.1 401 Unauthorized\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Digest realm=\"Oblivion\", nonce=\"nonce-value\"\r\n" "\r\n"; const char WebSocketStreamCreateDigestAuthTest::kAuthorizedRequest[] = "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" "Pragma: no-cache\r\n" "Cache-Control: no-cache\r\n" "Authorization: Digest username=\"FooBar\", realm=\"Oblivion\", " "nonce=\"nonce-value\", uri=\"/\", " "response=\"f72ff54ebde2f928860f806ec04acd1b\"\r\n" "Upgrade: websocket\r\n" "Origin: http://localhost\r\n" "Sec-WebSocket-Version: 13\r\n" "User-Agent:\r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: en-us,fr\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Extensions: permessage-deflate; " "client_max_window_bits\r\n" "\r\n"; class WebSocketStreamCreateUMATest : public ::testing::Test { public: // This enum should match with the enum in Delegate in websocket_stream.cc. enum HandshakeResult { INCOMPLETE, CONNECTED, FAILED, NUM_HANDSHAKE_RESULT_TYPES, }; class StreamCreation : public WebSocketStreamCreateTest { void TestBody() override {} }; scoped_ptr GetSamples(const std::string& name) { base::HistogramBase* histogram = base::StatisticsRecorder::FindHistogram(name); return histogram ? histogram->SnapshotSamples() : scoped_ptr(); } }; // Confirm that the basic case works as expected. TEST_F(WebSocketStreamCreateTest, SimpleSuccess) { CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", ""); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); EXPECT_TRUE(request_info_); EXPECT_TRUE(response_info_); } TEST_F(WebSocketStreamCreateTest, HandshakeInfo) { static const char kResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "foo: bar, baz\r\n" "hoge: fuga\r\n" "hoge: piyo\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kResponse); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); WaitUntilConnectDone(); EXPECT_TRUE(stream_); ASSERT_TRUE(request_info_); ASSERT_TRUE(response_info_); std::vector request_headers = RequestHeadersToVector(request_info_->headers); // We examine the contents of request_info_ and response_info_ // mainly only in this test case. EXPECT_EQ(GURL("ws://localhost/"), request_info_->url); EXPECT_EQ(GURL("ws://localhost/"), response_info_->url); EXPECT_EQ(101, response_info_->status_code); EXPECT_EQ("Switching Protocols", response_info_->status_text); ASSERT_EQ(12u, request_headers.size()); EXPECT_EQ(HeaderKeyValuePair("Host", "localhost"), request_headers[0]); EXPECT_EQ(HeaderKeyValuePair("Connection", "Upgrade"), request_headers[1]); EXPECT_EQ(HeaderKeyValuePair("Pragma", "no-cache"), request_headers[2]); EXPECT_EQ(HeaderKeyValuePair("Cache-Control", "no-cache"), request_headers[3]); EXPECT_EQ(HeaderKeyValuePair("Upgrade", "websocket"), request_headers[4]); EXPECT_EQ(HeaderKeyValuePair("Origin", "http://localhost"), request_headers[5]); EXPECT_EQ(HeaderKeyValuePair("Sec-WebSocket-Version", "13"), request_headers[6]); EXPECT_EQ(HeaderKeyValuePair("User-Agent", ""), request_headers[7]); EXPECT_EQ(HeaderKeyValuePair("Accept-Encoding", "gzip, deflate"), request_headers[8]); EXPECT_EQ(HeaderKeyValuePair("Accept-Language", "en-us,fr"), request_headers[9]); EXPECT_EQ("Sec-WebSocket-Key", request_headers[10].first); EXPECT_EQ(HeaderKeyValuePair("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits"), request_headers[11]); std::vector response_headers = ResponseHeadersToVector(*response_info_->headers.get()); ASSERT_EQ(6u, response_headers.size()); // Sort the headers for ease of verification. std::sort(response_headers.begin(), response_headers.end()); EXPECT_EQ(HeaderKeyValuePair("Connection", "Upgrade"), response_headers[0]); EXPECT_EQ("Sec-WebSocket-Accept", response_headers[1].first); EXPECT_EQ(HeaderKeyValuePair("Upgrade", "websocket"), response_headers[2]); EXPECT_EQ(HeaderKeyValuePair("foo", "bar, baz"), response_headers[3]); EXPECT_EQ(HeaderKeyValuePair("hoge", "fuga"), response_headers[4]); EXPECT_EQ(HeaderKeyValuePair("hoge", "piyo"), response_headers[5]); } // Confirm that the stream isn't established until the message loop runs. TEST_F(WebSocketStreamCreateTest, NeedsToRunLoop) { CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", ""); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); } // Check the path is used. TEST_F(WebSocketStreamCreateTest, PathIsUsed) { CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", NoSubProtocols(), LocalhostOrigin(), "", ""); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // Check that the origin is used. TEST_F(WebSocketStreamCreateTest, OriginIsUsed) { CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", NoSubProtocols(), GoogleOrigin(), "", ""); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // Check that sub-protocols are sent and parsed. TEST_F(WebSocketStreamCreateTest, SubProtocolIsUsed) { std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", sub_protocols, GoogleOrigin(), "Sec-WebSocket-Protocol: chatv11.chromium.org, " "chatv20.chromium.org\r\n", "Sec-WebSocket-Protocol: chatv20.chromium.org\r\n"); WaitUntilConnectDone(); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); EXPECT_EQ("chatv20.chromium.org", stream_->GetSubProtocol()); } // Unsolicited sub-protocols are rejected. TEST_F(WebSocketStreamCreateTest, UnsolicitedSubProtocol) { CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", NoSubProtocols(), GoogleOrigin(), "", "Sec-WebSocket-Protocol: chatv20.chromium.org\r\n"); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Response must not include 'Sec-WebSocket-Protocol' header " "if not present in request: chatv20.chromium.org", failure_message()); } // Missing sub-protocol response is rejected. TEST_F(WebSocketStreamCreateTest, UnacceptedSubProtocol) { std::vector sub_protocols; sub_protocols.push_back("chat.example.com"); CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", sub_protocols, LocalhostOrigin(), "Sec-WebSocket-Protocol: chat.example.com\r\n", ""); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Sent non-empty 'Sec-WebSocket-Protocol' header " "but no response was received", failure_message()); } // Only one sub-protocol can be accepted. TEST_F(WebSocketStreamCreateTest, MultipleSubProtocolsInResponse) { std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", sub_protocols, GoogleOrigin(), "Sec-WebSocket-Protocol: chatv11.chromium.org, " "chatv20.chromium.org\r\n", "Sec-WebSocket-Protocol: chatv11.chromium.org, " "chatv20.chromium.org\r\n"); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Protocol' header must not appear " "more than once in a response", failure_message()); } // Unmatched sub-protocol should be rejected. TEST_F(WebSocketStreamCreateTest, UnmatchedSubProtocolInResponse) { std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard("ws://localhost/testing_path", "localhost", "/testing_path", sub_protocols, GoogleOrigin(), "Sec-WebSocket-Protocol: chatv11.chromium.org, " "chatv20.chromium.org\r\n", "Sec-WebSocket-Protocol: chatv21.chromium.org\r\n"); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Protocol' header value 'chatv21.chromium.org' " "in response does not match any of sent values", failure_message()); } // permessage-deflate extension basic success case. TEST_F(WebSocketStreamCreateExtensionTest, PerMessageDeflateSuccess) { CreateAndConnectWithExtensions("permessage-deflate"); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); } // permessage-deflate extensions success with all parameters. TEST_F(WebSocketStreamCreateExtensionTest, PerMessageDeflateParamsSuccess) { CreateAndConnectWithExtensions( "permessage-deflate; client_no_context_takeover; " "server_max_window_bits=11; client_max_window_bits=13; " "server_no_context_takeover"); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); } // Verify that incoming messages are actually decompressed with // permessage-deflate enabled. TEST_F(WebSocketStreamCreateExtensionTest, PerMessageDeflateInflates) { CreateAndConnectCustomResponse( "ws://localhost/testing_path", "localhost", "/testing_path", NoSubProtocols(), LocalhostOrigin(), "", WebSocketStandardResponse( "Sec-WebSocket-Extensions: permessage-deflate\r\n") + std::string( "\xc1\x07" // WebSocket header (FIN + RSV1, Text payload 7 bytes) "\xf2\x48\xcd\xc9\xc9\x07\x00", // "Hello" DEFLATE compressed 9)); WaitUntilConnectDone(); ASSERT_TRUE(stream_); std::vector> frames; CompletionCallback callback; ASSERT_EQ(OK, stream_->ReadFrames(&frames, callback)); ASSERT_EQ(1U, frames.size()); ASSERT_EQ(5U, frames[0]->header.payload_length); EXPECT_EQ("Hello", std::string(frames[0]->data->data(), 5)); } // Unknown extension in the response is rejected TEST_F(WebSocketStreamCreateExtensionTest, UnknownExtension) { CreateAndConnectWithExtensions("x-unknown-extension"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Found an unsupported extension 'x-unknown-extension' " "in 'Sec-WebSocket-Extensions' header", failure_message()); } // Malformed extensions are rejected (this file does not cover all possible // parse failures, as the parser is covered thoroughly by its own unit tests). TEST_F(WebSocketStreamCreateExtensionTest, MalformedExtension) { CreateAndConnectWithExtensions(";"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: 'Sec-WebSocket-Extensions' header " "value is rejected by the parser: ;", failure_message()); } // The permessage-deflate extension may only be specified once. TEST_F(WebSocketStreamCreateExtensionTest, OnlyOnePerMessageDeflateAllowed) { CreateAndConnectWithExtensions( "permessage-deflate, permessage-deflate; client_max_window_bits=10"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: " "Received duplicate permessage-deflate response", failure_message()); } // client_max_window_bits must have an argument TEST_F(WebSocketStreamCreateExtensionTest, NoMaxWindowBitsArgument) { CreateAndConnectWithExtensions("permessage-deflate; client_max_window_bits"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: Error in permessage-deflate: " "client_max_window_bits must have value", failure_message()); } // Other cases for permessage-deflate parameters are tested in // websocket_deflate_parameters_test.cc. // TODO(ricea): Check that WebSocketDeflateStream is initialised with the // arguments from the server. This is difficult because the data written to the // socket is randomly masked. // Additional Sec-WebSocket-Accept headers should be rejected. TEST_F(WebSocketStreamCreateTest, DoubleAccept) { CreateAndConnectStandard( "ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Accept' header must not appear " "more than once in a response", failure_message()); } // Response code 200 must be rejected. TEST_F(WebSocketStreamCreateTest, InvalidStatusCode) { static const char kInvalidStatusCodeResponse[] = "HTTP/1.1 200 OK\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kInvalidStatusCodeResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 200", failure_message()); } // Redirects are not followed (according to the WHATWG WebSocket API, which // overrides RFC6455 for browser applications). TEST_F(WebSocketStreamCreateTest, RedirectsRejected) { static const char kRedirectResponse[] = "HTTP/1.1 302 Moved Temporarily\r\n" "Content-Type: text/html\r\n" "Content-Length: 34\r\n" "Connection: keep-alive\r\n" "Location: ws://localhost/other\r\n" "\r\n" "Moved

Moved

"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kRedirectResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 302", failure_message()); } // Malformed responses should be rejected. HttpStreamParser will accept just // about any garbage in the middle of the headers. To make it give up, the junk // has to be at the start of the response. Even then, it just gets treated as an // HTTP/0.9 response. TEST_F(WebSocketStreamCreateTest, MalformedResponse) { static const char kMalformedResponse[] = "220 mx.google.com ESMTP\r\n" "HTTP/1.1 101 OK\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMalformedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: Invalid status line", failure_message()); } // Upgrade header must be present. TEST_F(WebSocketStreamCreateTest, MissingUpgradeHeader) { static const char kMissingUpgradeResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMissingUpgradeResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: 'Upgrade' header is missing", failure_message()); } // There must only be one upgrade header. TEST_F(WebSocketStreamCreateTest, DoubleUpgradeHeader) { CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", "Upgrade: HTTP/2.0\r\n"); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Upgrade' header must not appear more than once in a response", failure_message()); } // There must only be one correct upgrade header. TEST_F(WebSocketStreamCreateTest, IncorrectUpgradeHeader) { static const char kMissingUpgradeResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Upgrade: hogefuga\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMissingUpgradeResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Upgrade' header value is not 'WebSocket': hogefuga", failure_message()); } // Connection header must be present. TEST_F(WebSocketStreamCreateTest, MissingConnectionHeader) { static const char kMissingConnectionResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMissingConnectionResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Connection' header is missing", failure_message()); } // Connection header must contain "Upgrade". TEST_F(WebSocketStreamCreateTest, IncorrectConnectionHeader) { static const char kMissingConnectionResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Connection: hogefuga\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMissingConnectionResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Connection' header value must contain 'Upgrade'", failure_message()); } // Connection header is permitted to contain other tokens. TEST_F(WebSocketStreamCreateTest, AdditionalTokenInConnectionHeader) { static const char kAdditionalConnectionTokenResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade, Keep-Alive\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kAdditionalConnectionTokenResponse); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // Sec-WebSocket-Accept header must be present. TEST_F(WebSocketStreamCreateTest, MissingSecWebSocketAccept) { static const char kMissingAcceptResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kMissingAcceptResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Accept' header is missing", failure_message()); } // Sec-WebSocket-Accept header must match the key that was sent. TEST_F(WebSocketStreamCreateTest, WrongSecWebSocketAccept) { static const char kIncorrectAcceptResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: x/byyPZ2tOFvJCGkkugcKvqhhPk=\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kIncorrectAcceptResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Incorrect 'Sec-WebSocket-Accept' header value", failure_message()); } // Cancellation works. TEST_F(WebSocketStreamCreateTest, Cancellation) { CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", ""); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); } // Connect failure must look just like negotiation failure. TEST_F(WebSocketStreamCreateTest, ConnectionFailure) { scoped_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_REFUSED", failure_message()); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); } // Connect timeout must look just like any other failure. TEST_F(WebSocketStreamCreateTest, ConnectionTimeout) { scoped_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(ASYNC, ERR_CONNECTION_TIMED_OUT)); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_TIMED_OUT", failure_message()); } // The server doesn't respond to the opening handshake. TEST_F(WebSocketStreamCreateTest, HandshakeTimeout) { scoped_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); scoped_ptr timer(new MockWeakTimer(false, false)); base::WeakPtr weak_timer = timer->AsWeakPtr(); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data), std::move(timer)); EXPECT_FALSE(has_failed()); ASSERT_TRUE(weak_timer.get()); EXPECT_TRUE(weak_timer->IsRunning()); weak_timer->Fire(); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("WebSocket opening handshake timed out", failure_message()); ASSERT_TRUE(weak_timer.get()); EXPECT_FALSE(weak_timer->IsRunning()); } // When the connection establishes the timer should be stopped. TEST_F(WebSocketStreamCreateTest, HandshakeTimerOnSuccess) { scoped_ptr timer(new MockWeakTimer(false, false)); base::WeakPtr weak_timer = timer->AsWeakPtr(); CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", "", std::move(timer)); ASSERT_TRUE(weak_timer); EXPECT_TRUE(weak_timer->IsRunning()); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(weak_timer); EXPECT_FALSE(weak_timer->IsRunning()); } // When the connection fails the timer should be stopped. TEST_F(WebSocketStreamCreateTest, HandshakeTimerOnFailure) { scoped_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); scoped_ptr timer(new MockWeakTimer(false, false)); base::WeakPtr weak_timer = timer->AsWeakPtr(); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data), std::move(timer)); ASSERT_TRUE(weak_timer.get()); EXPECT_TRUE(weak_timer->IsRunning()); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_REFUSED", failure_message()); ASSERT_TRUE(weak_timer.get()); EXPECT_FALSE(weak_timer->IsRunning()); } // Cancellation during connect works. TEST_F(WebSocketStreamCreateTest, CancellationDuringConnect) { scoped_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); } // Cancellation during write of the request headers works. TEST_F(WebSocketStreamCreateTest, CancellationDuringWrite) { // First write never completes. MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_IO_PENDING, 0)}; SequencedSocketData* socket_data( new SequencedSocketData(NULL, 0, writes, arraysize(writes))); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), make_scoped_ptr(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data->AllWriteDataConsumed()); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_TRUE(request_info_); EXPECT_FALSE(response_info_); } // Cancellation during read of the response headers works. TEST_F(WebSocketStreamCreateTest, CancellationDuringRead) { std::string request = WebSocketStandardRequest("/", "localhost", LocalhostOrigin(), ""); MockWrite writes[] = {MockWrite(ASYNC, 0, request.c_str())}; MockRead reads[] = { MockRead(SYNCHRONOUS, ERR_IO_PENDING, 1), }; scoped_ptr socket_data(BuildSocketData(reads, writes)); SequencedSocketData* socket_data_raw_ptr = socket_data.get(); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data_raw_ptr->AllReadDataConsumed()); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_TRUE(request_info_); EXPECT_FALSE(response_info_); } // Over-size response headers (> 256KB) should not cause a crash. This is a // regression test for crbug.com/339456. It is based on the layout test // "cookie-flood.html". TEST_F(WebSocketStreamCreateTest, VeryLargeResponseHeaders) { std::string set_cookie_headers; set_cookie_headers.reserve(45 * 10000); for (int i = 0; i < 10000; ++i) { set_cookie_headers += base::StringPrintf("Set-Cookie: WK-websocket-test-flood-%d=1\r\n", i); } CreateAndConnectStandard("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", set_cookie_headers); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_FALSE(response_info_); } // If the remote host closes the connection without sending headers, we should // log the console message "Connection closed before receiving a handshake // response". TEST_F(WebSocketStreamCreateTest, NoResponse) { std::string request = WebSocketStandardRequest("/", "localhost", LocalhostOrigin(), ""); MockWrite writes[] = {MockWrite(ASYNC, request.data(), request.size(), 0)}; MockRead reads[] = {MockRead(ASYNC, 0, 1)}; scoped_ptr socket_data(BuildSocketData(reads, writes)); SequencedSocketData* socket_data_raw_ptr = socket_data.get(); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data_raw_ptr->AllReadDataConsumed()); EXPECT_TRUE(has_failed()); EXPECT_FALSE(stream_); EXPECT_FALSE(response_info_); EXPECT_EQ("Connection closed before receiving a handshake response", failure_message()); } TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateFailure) { ssl_data_.push_back(make_scoped_ptr( new SSLSocketDataProvider(ASYNC, ERR_CERT_AUTHORITY_INVALID))); ssl_data_[0]->cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_data_[0]->cert.get()); scoped_ptr raw_socket_data(BuildNullSocketData()); CreateAndConnectRawExpectations("wss://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(raw_socket_data)); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); ASSERT_TRUE(ssl_error_callbacks_); ssl_error_callbacks_->CancelSSLRequest(ERR_CERT_AUTHORITY_INVALID, &ssl_info_); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); } TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateSuccess) { scoped_ptr ssl_data( new SSLSocketDataProvider(ASYNC, ERR_CERT_AUTHORITY_INVALID)); ssl_data->cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_data->cert.get()); ssl_data_.push_back(std::move(ssl_data)); ssl_data.reset(new SSLSocketDataProvider(ASYNC, OK)); ssl_data_.push_back(std::move(ssl_data)); url_request_context_host_.AddRawExpectations(BuildNullSocketData()); CreateAndConnectStandard("wss://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", ""); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); ASSERT_TRUE(ssl_error_callbacks_); ssl_error_callbacks_->ContinueSSLRequest(); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // If the server requests authorisation, but we have no credentials, the // connection should fail cleanly. TEST_F(WebSocketStreamCreateBasicAuthTest, FailureNoCredentials) { CreateAndConnectCustomResponse("ws://localhost/", "localhost", "/", NoSubProtocols(), LocalhostOrigin(), "", kUnauthorizedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("HTTP Authentication failed; no valid credentials available", failure_message()); EXPECT_TRUE(response_info_); } TEST_F(WebSocketStreamCreateBasicAuthTest, SuccessPasswordInUrl) { CreateAndConnectAuthHandshake("ws://foo:bar@localhost/", "Zm9vOmJhcg==", WebSocketStandardResponse(std::string())); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(response_info_); EXPECT_EQ(101, response_info_->status_code); } TEST_F(WebSocketStreamCreateBasicAuthTest, FailureIncorrectPasswordInUrl) { CreateAndConnectAuthHandshake( "ws://foo:baz@localhost/", "Zm9vOmJheg==", kUnauthorizedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_TRUE(response_info_); } // Digest auth has the same connection semantics as Basic auth, so we can // generally assume that whatever works for Basic auth will also work for // Digest. There's just one test here, to confirm that it works at all. TEST_F(WebSocketStreamCreateDigestAuthTest, DigestPasswordInUrl) { AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse)); CreateAndConnectRawExpectations( "ws://FooBar:pass@localhost/", NoSubProtocols(), LocalhostOrigin(), helper_.BuildSocketData2(kAuthorizedRequest, WebSocketStandardResponse(std::string()))); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(response_info_); EXPECT_EQ(101, response_info_->status_code); } TEST_F(WebSocketStreamCreateUMATest, Incomplete) { const std::string name("Net.WebSocket.HandshakeResult"); scoped_ptr original(GetSamples(name)); { StreamCreation creation; creation.CreateAndConnectStandard("ws://localhost/", "localhost", "/", creation.NoSubProtocols(), LocalhostOrigin(), "", ""); } scoped_ptr samples(GetSamples(name)); ASSERT_TRUE(samples); if (original) { samples->Subtract(*original); // Cancel the original values. } EXPECT_EQ(1, samples->GetCount(INCOMPLETE)); EXPECT_EQ(0, samples->GetCount(CONNECTED)); EXPECT_EQ(0, samples->GetCount(FAILED)); } TEST_F(WebSocketStreamCreateUMATest, Connected) { const std::string name("Net.WebSocket.HandshakeResult"); scoped_ptr original(GetSamples(name)); { StreamCreation creation; creation.CreateAndConnectStandard("ws://localhost/", "localhost", "/", creation.NoSubProtocols(), LocalhostOrigin(), "", ""); creation.WaitUntilConnectDone(); } scoped_ptr samples(GetSamples(name)); ASSERT_TRUE(samples); if (original) { samples->Subtract(*original); // Cancel the original values. } EXPECT_EQ(0, samples->GetCount(INCOMPLETE)); EXPECT_EQ(1, samples->GetCount(CONNECTED)); EXPECT_EQ(0, samples->GetCount(FAILED)); } TEST_F(WebSocketStreamCreateUMATest, Failed) { const std::string name("Net.WebSocket.HandshakeResult"); scoped_ptr original(GetSamples(name)); { StreamCreation creation; static const char kInvalidStatusCodeResponse[] = "HTTP/1.1 200 OK\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; creation.CreateAndConnectCustomResponse( "ws://localhost/", "localhost", "/", creation.NoSubProtocols(), LocalhostOrigin(), "", kInvalidStatusCodeResponse); creation.WaitUntilConnectDone(); } scoped_ptr samples(GetSamples(name)); ASSERT_TRUE(samples); if (original) { samples->Subtract(*original); // Cancel the original values. } EXPECT_EQ(1, samples->GetCount(INCOMPLETE)); EXPECT_EQ(0, samples->GetCount(CONNECTED)); EXPECT_EQ(0, samples->GetCount(FAILED)); } TEST_F(WebSocketStreamCreateTest, HandleErrConnectionClosed) { static const char kTruncatedResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Cache-Control: no-sto"; std::string request = WebSocketStandardRequest("/", "localhost", LocalhostOrigin(), ""); MockRead reads[] = { MockRead(SYNCHRONOUS, 1, kTruncatedResponse), MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED, 2), }; MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, request.c_str())}; scoped_ptr socket_data(BuildSocketData(reads, writes)); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); } TEST_F(WebSocketStreamCreateTest, HandleErrTunnelConnectionFailed) { static const char kConnectRequest[] = "CONNECT localhost:80 HTTP/1.1\r\n" "Host: localhost:80\r\n" "Proxy-Connection: keep-alive\r\n" "\r\n"; static const char kProxyResponse[] = "HTTP/1.1 403 Forbidden\r\n" "Content-Type: text/html\r\n" "Content-Length: 9\r\n" "Connection: keep-alive\r\n" "\r\n" "Forbidden"; MockRead reads[] = {MockRead(SYNCHRONOUS, 1, kProxyResponse)}; MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, kConnectRequest)}; scoped_ptr socket_data(BuildSocketData(reads, writes)); url_request_context_host_.SetProxyConfig("https=proxy:8000"); CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), LocalhostOrigin(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Establishing a tunnel via proxy server failed.", failure_message()); } } // namespace } // namespace net