diff options
-rw-r--r-- | net/net.gyp | 2 | ||||
-rw-r--r-- | net/websockets/README | 2 | ||||
-rw-r--r-- | net/websockets/websocket_basic_handshake_stream.cc | 13 | ||||
-rw-r--r-- | net/websockets/websocket_basic_handshake_stream.h | 9 | ||||
-rw-r--r-- | net/websockets/websocket_handshake_stream_create_helper_test.cc | 145 | ||||
-rw-r--r-- | net/websockets/websocket_stream.h | 2 | ||||
-rw-r--r-- | net/websockets/websocket_stream_test.cc | 515 | ||||
-rw-r--r-- | net/websockets/websocket_test_util.cc | 97 | ||||
-rw-r--r-- | net/websockets/websocket_test_util.h | 71 |
9 files changed, 854 insertions, 2 deletions
diff --git a/net/net.gyp b/net/net.gyp index 87c95c0..6952ea6 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -1972,9 +1972,11 @@ 'websockets/websocket_frame_test.cc', 'websockets/websocket_handshake_handler_spdy_test.cc', 'websockets/websocket_handshake_handler_test.cc', + 'websockets/websocket_handshake_stream_create_helper_test.cc', 'websockets/websocket_inflater_test.cc', 'websockets/websocket_job_test.cc', 'websockets/websocket_net_log_params_test.cc', + 'websockets/websocket_stream_test.cc', 'websockets/websocket_test_util.cc', 'websockets/websocket_test_util.h', 'websockets/websocket_throttle_test.cc', diff --git a/net/websockets/README b/net/websockets/README index 9e20097..fab4c20 100644 --- a/net/websockets/README +++ b/net/websockets/README @@ -63,12 +63,14 @@ websocket_frame_test.cc websocket_handshake_stream_base.h websocket_handshake_stream_create_helper.cc websocket_handshake_stream_create_helper.h +websocket_handshake_stream_create_helper_test.cc websocket_inflater.cc websocket_inflater.h websocket_inflater_test.cc websocket_mux.h websocket_stream.cc websocket_stream.h +websocket_stream_test.cc websocket_test_util.cc websocket_test_util.h diff --git a/net/websockets/websocket_basic_handshake_stream.cc b/net/websockets/websocket_basic_handshake_stream.cc index 1356461..1a8154b 100644 --- a/net/websockets/websocket_basic_handshake_stream.cc +++ b/net/websockets/websocket_basic_handshake_stream.cc @@ -144,7 +144,13 @@ int WebSocketBasicHandshakeStream::SendRequest( // Sec-WebSockey-Key header. HttpRequestHeaders enriched_headers; enriched_headers.CopyFrom(headers); - std::string handshake_challenge = GenerateHandshakeChallenge(); + std::string handshake_challenge; + if (handshake_challenge_for_testing_) { + handshake_challenge = *handshake_challenge_for_testing_; + handshake_challenge_for_testing_.reset(); + } else { + handshake_challenge = GenerateHandshakeChallenge(); + } enriched_headers.SetHeader(websockets::kSecWebSocketKey, handshake_challenge); AddVectorHeaderIfNonEmpty(websockets::kSecWebSocketProtocol, @@ -253,6 +259,11 @@ scoped_ptr<WebSocketStream> WebSocketBasicHandshakeStream::Upgrade() { extensions_)); } +void WebSocketBasicHandshakeStream::SetWebSocketKeyForTesting( + const std::string& key) { + handshake_challenge_for_testing_.reset(new std::string(key)); +} + void WebSocketBasicHandshakeStream::ReadResponseHeadersCallback( const CompletionCallback& callback, int result) { diff --git a/net/websockets/websocket_basic_handshake_stream.h b/net/websockets/websocket_basic_handshake_stream.h index 5774b72..69deca2 100644 --- a/net/websockets/websocket_basic_handshake_stream.h +++ b/net/websockets/websocket_basic_handshake_stream.h @@ -66,6 +66,11 @@ class NET_EXPORT_PRIVATE WebSocketBasicHandshakeStream // Upgrade() has been called and should be disposed of as soon as possible. virtual scoped_ptr<WebSocketStream> Upgrade() OVERRIDE; + // Set the value used for the next Sec-WebSocket-Key header + // deterministically. The key is only used once, and then discarded. + // For tests only. + void SetWebSocketKeyForTesting(const std::string& key); + private: // A wrapper for the ReadResponseHeaders callback that checks whether or not // the connection has been accepted. @@ -89,6 +94,10 @@ class NET_EXPORT_PRIVATE WebSocketBasicHandshakeStream // This is stored in SendRequest() for use by ReadResponseHeaders(). HttpResponseInfo* http_response_info_; + // The key to be sent in the next Sec-WebSocket-Key header. Usually NULL (the + // key is generated on the fly). + scoped_ptr<std::string> handshake_challenge_for_testing_; + // The required value for the Sec-WebSocket-Accept header. std::string handshake_challenge_response_; diff --git a/net/websockets/websocket_handshake_stream_create_helper_test.cc b/net/websockets/websocket_handshake_stream_create_helper_test.cc new file mode 100644 index 0000000..7566edf --- /dev/null +++ b/net/websockets/websocket_handshake_stream_create_helper_test.cc @@ -0,0 +1,145 @@ +// 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_handshake_stream_create_helper.h" + +#include "net/base/completion_callback.h" +#include "net/base/net_errors.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_request_info.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_response_info.h" +#include "net/socket/client_socket_handle.h" +#include "net/socket/socket_test_util.h" +#include "net/websockets/websocket_basic_handshake_stream.h" +#include "net/websockets/websocket_stream.h" +#include "net/websockets/websocket_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { +namespace { + +// This class encapsulates the details of creating a mock ClientSocketHandle. +class MockClientSocketHandleFactory { + public: + MockClientSocketHandleFactory() + : histograms_("a"), + pool_(1, 1, &histograms_, socket_factory_maker_.factory()) {} + + // The created socket expects |expect_written| to be written to the socket, + // and will respond with |return_to_read|. The test will fail if the expected + // text is not written, or if all the bytes are not read. + scoped_ptr<ClientSocketHandle> CreateClientSocketHandle( + const std::string& expect_written, + const std::string& return_to_read) { + socket_factory_maker_.SetExpectations(expect_written, return_to_read); + scoped_ptr<ClientSocketHandle> socket_handle(new ClientSocketHandle); + socket_handle->Init( + "a", + scoped_refptr<MockTransportSocketParams>(), + MEDIUM, + CompletionCallback(), + &pool_, + BoundNetLog()); + return socket_handle.Pass(); + } + + private: + WebSocketDeterministicMockClientSocketFactoryMaker socket_factory_maker_; + ClientSocketPoolHistograms histograms_; + MockTransportClientSocketPool pool_; + + DISALLOW_COPY_AND_ASSIGN(MockClientSocketHandleFactory); +}; + +class WebSocketHandshakeStreamCreateHelperTest : public ::testing::Test { + protected: + scoped_ptr<WebSocketStream> CreateAndInitializeStream( + const std::string& socket_url, + const std::string& socket_path, + const std::vector<std::string>& sub_protocols, + const std::string& origin, + const std::string& extra_request_headers, + const std::string& extra_response_headers) { + WebSocketHandshakeStreamCreateHelper create_helper(sub_protocols); + + scoped_ptr<ClientSocketHandle> socket_handle = + socket_handle_factory_.CreateClientSocketHandle( + WebSocketStandardRequest( + socket_path, origin, extra_request_headers), + WebSocketStandardResponse(extra_response_headers)); + + scoped_ptr<WebSocketHandshakeStreamBase> handshake( + create_helper.CreateBasicStream(socket_handle.Pass(), false)); + + // If in future the implementation type returned by CreateBasicStream() + // changes, this static_cast will be wrong. However, in that case the test + // will fail and AddressSanitizer should identify the issue. + static_cast<WebSocketBasicHandshakeStream*>(handshake.get()) + ->SetWebSocketKeyForTesting("dGhlIHNhbXBsZSBub25jZQ=="); + + HttpRequestInfo request_info; + request_info.url = GURL(socket_url); + request_info.method = "GET"; + request_info.load_flags = LOAD_DISABLE_CACHE | LOAD_DO_NOT_PROMPT_FOR_LOGIN; + int rv = handshake->InitializeStream( + &request_info, DEFAULT_PRIORITY, BoundNetLog(), CompletionCallback()); + EXPECT_EQ(OK, rv); + + HttpRequestHeaders headers; + headers.SetHeader("Host", "localhost"); + headers.SetHeader("Connection", "Upgrade"); + headers.SetHeader("Upgrade", "websocket"); + headers.SetHeader("Origin", origin); + headers.SetHeader("Sec-WebSocket-Version", "13"); + headers.SetHeader("User-Agent", ""); + headers.SetHeader("Accept-Encoding", "gzip,deflate"); + headers.SetHeader("Accept-Language", "en-us,fr"); + + HttpResponseInfo response; + TestCompletionCallback dummy; + + rv = handshake->SendRequest(headers, &response, dummy.callback()); + + EXPECT_EQ(OK, rv); + + rv = handshake->ReadResponseHeaders(dummy.callback()); + EXPECT_EQ(OK, rv); + EXPECT_EQ(101, response.headers->response_code()); + EXPECT_TRUE(response.headers->HasHeaderValue("Connection", "Upgrade")); + EXPECT_TRUE(response.headers->HasHeaderValue("Upgrade", "websocket")); + return handshake->Upgrade(); + } + + MockClientSocketHandleFactory socket_handle_factory_; +}; + +// Confirm that the basic case works as expected. +TEST_F(WebSocketHandshakeStreamCreateHelperTest, BasicStream) { + scoped_ptr<WebSocketStream> stream = + CreateAndInitializeStream("ws://localhost/", "/", + std::vector<std::string>(), "http://localhost/", + "", ""); + EXPECT_EQ("", stream->GetExtensions()); + EXPECT_EQ("", stream->GetSubProtocol()); +} + +// Verify that the sub-protocols are passed through. +TEST_F(WebSocketHandshakeStreamCreateHelperTest, SubProtocols) { + std::vector<std::string> sub_protocols; + sub_protocols.push_back("chat"); + sub_protocols.push_back("superchat"); + scoped_ptr<WebSocketStream> stream = + CreateAndInitializeStream("ws://localhost/", "/", + sub_protocols, "http://localhost/", + "Sec-WebSocket-Protocol: chat, superchat\r\n", + "Sec-WebSocket-Protocol: superchat\r\n"); + EXPECT_EQ("superchat", stream->GetSubProtocol()); +} + +// TODO(ricea): Test extensions once they are implemented. + +} // namespace +} // namespace net diff --git a/net/websockets/websocket_stream.h b/net/websockets/websocket_stream.h index c5f6f97..c08f8dc 100644 --- a/net/websockets/websocket_stream.h +++ b/net/websockets/websocket_stream.h @@ -50,7 +50,7 @@ class NET_EXPORT_PRIVATE WebSocketStream { public: // A concrete object derived from ConnectDelegate is supplied by the caller to // CreateAndConnectStream() to receive the result of the connection. - class ConnectDelegate { + class NET_EXPORT_PRIVATE ConnectDelegate { public: virtual ~ConnectDelegate(); // Called on successful connection. The parameter is an object derived from diff --git a/net/websockets/websocket_stream_test.cc b/net/websockets/websocket_stream_test.cc new file mode 100644 index 0000000..3e11a95 --- /dev/null +++ b/net/websockets/websocket_stream_test.cc @@ -0,0 +1,515 @@ +// 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 <string> +#include <vector> + +#include "base/run_loop.h" +#include "net/base/net_errors.h" +#include "net/socket/client_socket_handle.h" +#include "net/socket/socket_test_util.h" +#include "net/url_request/url_request_test_util.h" +#include "net/websockets/websocket_basic_handshake_stream.h" +#include "net/websockets/websocket_handshake_stream_create_helper.h" +#include "net/websockets/websocket_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace net { +namespace { + +// A sub-class of WebSocketHandshakeStreamCreateHelper which always sets a +// deterministic key to use in the WebSocket handshake. +class DeterministicKeyWebSocketHandshakeStreamCreateHelper + : public WebSocketHandshakeStreamCreateHelper { + public: + DeterministicKeyWebSocketHandshakeStreamCreateHelper( + const std::vector<std::string>& requested_subprotocols) + : WebSocketHandshakeStreamCreateHelper(requested_subprotocols) {} + + virtual WebSocketHandshakeStreamBase* CreateBasicStream( + scoped_ptr<ClientSocketHandle> connection, + bool using_proxy) OVERRIDE { + WebSocketHandshakeStreamCreateHelper::CreateBasicStream(connection.Pass(), + using_proxy); + // This will break in an obvious way if the type created by + // CreateBasicStream() changes. + static_cast<WebSocketBasicHandshakeStream*>(stream()) + ->SetWebSocketKeyForTesting("dGhlIHNhbXBsZSBub25jZQ=="); + return stream(); + } +}; + +class WebSocketStreamCreateTest : public ::testing::Test { + protected: + WebSocketStreamCreateTest() : websocket_error_(0) {} + + void CreateAndConnectCustomResponse( + const std::string& socket_url, + const std::string& socket_path, + const std::vector<std::string>& sub_protocols, + const std::string& origin, + const std::string& extra_request_headers, + const std::string& response_body) { + url_request_context_host_.SetExpectations( + WebSocketStandardRequest(socket_path, origin, extra_request_headers), + response_body); + CreateAndConnectStream(socket_url, sub_protocols, origin); + } + + // |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_path, + const std::vector<std::string>& sub_protocols, + const std::string& origin, + const std::string& extra_request_headers, + const std::string& extra_response_headers) { + CreateAndConnectCustomResponse( + socket_url, + socket_path, + sub_protocols, + origin, + extra_request_headers, + WebSocketStandardResponse(extra_response_headers)); + } + + void CreateAndConnectRawExpectations( + const std::string& socket_url, + const std::vector<std::string>& sub_protocols, + const std::string& origin, + scoped_ptr<DeterministicSocketData> socket_data) { + url_request_context_host_.SetRawExpectations(socket_data.Pass()); + CreateAndConnectStream(socket_url, sub_protocols, origin); + } + + // A wrapper for CreateAndConnectStreamForTesting that knows about our default + // parameters. + void CreateAndConnectStream(const std::string& socket_url, + const std::vector<std::string>& sub_protocols, + const std::string& origin) { + stream_request_ = ::net::CreateAndConnectStreamForTesting( + GURL(socket_url), + scoped_ptr<WebSocketHandshakeStreamCreateHelper>( + new DeterministicKeyWebSocketHandshakeStreamCreateHelper( + sub_protocols)), + GURL(origin), + url_request_context_host_.GetURLRequestContext(), + BoundNetLog(), + scoped_ptr<WebSocketStream::ConnectDelegate>( + new TestConnectDelegate(this))); + } + + static void RunUntilIdle() { base::RunLoop().RunUntilIdle(); } + + // A simple function to make the tests more readable. Creates an empty vector. + static std::vector<std::string> NoSubProtocols() { + return std::vector<std::string>(); + } + + uint16 error() const { return websocket_error_; } + + class TestConnectDelegate : public WebSocketStream::ConnectDelegate { + public: + TestConnectDelegate(WebSocketStreamCreateTest* owner) : owner_(owner) {} + + virtual void OnSuccess(scoped_ptr<WebSocketStream> stream) OVERRIDE { + stream.swap(owner_->stream_); + } + + virtual void OnFailure(uint16 websocket_error) OVERRIDE { + owner_->websocket_error_ = websocket_error; + } + + private: + WebSocketStreamCreateTest* owner_; + }; + + WebSocketTestURLRequestContextHost url_request_context_host_; + scoped_ptr<WebSocketStreamRequest> stream_request_; + // Only set if the connection succeeded. + scoped_ptr<WebSocketStream> stream_; + // Only set if the connection failed. 0 otherwise. + uint16 websocket_error_; +}; + +// Confirm that the basic case works as expected. +TEST_F(WebSocketStreamCreateTest, SimpleSuccess) { + CreateAndConnectStandard( + "ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", ""); + RunUntilIdle(); + EXPECT_TRUE(stream_); +} + +// Confirm that the stream isn't established until the message loop runs. +TEST_F(WebSocketStreamCreateTest, NeedsToRunLoop) { + CreateAndConnectStandard( + "ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", ""); + EXPECT_FALSE(stream_); +} + +// Check the path is used. +TEST_F(WebSocketStreamCreateTest, PathIsUsed) { + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + NoSubProtocols(), + "http://localhost/", + "", + ""); + RunUntilIdle(); + EXPECT_TRUE(stream_); +} + +// Check that the origin is used. +TEST_F(WebSocketStreamCreateTest, OriginIsUsed) { + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + NoSubProtocols(), + "http://google.com/", + "", + ""); + RunUntilIdle(); + EXPECT_TRUE(stream_); +} + +// Check that sub-protocols are sent and parsed. +TEST_F(WebSocketStreamCreateTest, SubProtocolIsUsed) { + std::vector<std::string> sub_protocols; + sub_protocols.push_back("chatv11.chromium.org"); + sub_protocols.push_back("chatv20.chromium.org"); + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + sub_protocols, + "http://google.com/", + "Sec-WebSocket-Protocol: chatv11.chromium.org, " + "chatv20.chromium.org\r\n", + "Sec-WebSocket-Protocol: chatv20.chromium.org\r\n"); + RunUntilIdle(); + EXPECT_TRUE(stream_); + EXPECT_EQ("chatv20.chromium.org", stream_->GetSubProtocol()); +} + +// Unsolicited sub-protocols are rejected. +TEST_F(WebSocketStreamCreateTest, UnsolicitedSubProtocol) { + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + NoSubProtocols(), + "http://google.com/", + "", + "Sec-WebSocket-Protocol: chatv20.chromium.org\r\n"); + RunUntilIdle(); + EXPECT_FALSE(stream_); + EXPECT_EQ(1006, error()); +} + +// Missing sub-protocol response is rejected. +TEST_F(WebSocketStreamCreateTest, UnacceptedSubProtocol) { + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + std::vector<std::string>(1, "chat.example.com"), + "http://localhost/", + "Sec-WebSocket-Protocol: chat.example.com\r\n", + ""); + RunUntilIdle(); + EXPECT_FALSE(stream_); + EXPECT_EQ(1006, error()); +} + +// Only one sub-protocol can be accepted. +TEST_F(WebSocketStreamCreateTest, MultipleSubProtocolsInResponse) { + std::vector<std::string> sub_protocols; + sub_protocols.push_back("chatv11.chromium.org"); + sub_protocols.push_back("chatv20.chromium.org"); + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + sub_protocols, + "http://google.com/", + "Sec-WebSocket-Protocol: chatv11.chromium.org, " + "chatv20.chromium.org\r\n", + "Sec-WebSocket-Protocol: chatv11.chromium.org, " + "chatv20.chromium.org\r\n"); + RunUntilIdle(); + EXPECT_FALSE(stream_); + EXPECT_EQ(1006, error()); +} + +// Unknown extension in the response is rejected +TEST_F(WebSocketStreamCreateTest, UnknownExtension) { + CreateAndConnectStandard("ws://localhost/testing_path", + "/testing_path", + NoSubProtocols(), + "http://localhost/", + "", + "Sec-WebSocket-Extensions: x-unknown-extension\r\n"); + RunUntilIdle(); + EXPECT_FALSE(stream_); + EXPECT_EQ(1006, error()); +} + +// Additional Sec-WebSocket-Accept headers should be rejected. +TEST_F(WebSocketStreamCreateTest, DoubleAccept) { + CreateAndConnectStandard( + "ws://localhost/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"); + RunUntilIdle(); + EXPECT_FALSE(stream_); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kInvalidStatusCodeResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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" + "<title>Moved</title><h1>Moved</h1>"; + CreateAndConnectCustomResponse("ws://localhost/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kRedirectResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kMalformedResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kMissingUpgradeResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// There must only be one upgrade header. +TEST_F(WebSocketStreamCreateTest, DoubleUpgradeHeader) { + CreateAndConnectStandard( + "ws://localhost/", + "/", + NoSubProtocols(), + "http://localhost/", + "", "Upgrade: HTTP/2.0\r\n"); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kMissingConnectionResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kAdditionalConnectionTokenResponse); + RunUntilIdle(); + 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kMissingAcceptResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// 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/", + "/", + NoSubProtocols(), + "http://localhost/", + "", + kIncorrectAcceptResponse); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// Cancellation works. +TEST_F(WebSocketStreamCreateTest, Cancellation) { + CreateAndConnectStandard( + "ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", ""); + stream_request_.reset(); + RunUntilIdle(); + EXPECT_FALSE(stream_); +} + +// Connect failure must look just like negotiation failure. +TEST_F(WebSocketStreamCreateTest, ConnectionFailure) { + scoped_ptr<DeterministicSocketData> socket_data( + new DeterministicSocketData(NULL, 0, NULL, 0)); + socket_data->set_connect_data( + MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); + CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), + "http://localhost/", socket_data.Pass()); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// Connect timeout must look just like any other failure. +TEST_F(WebSocketStreamCreateTest, ConnectionTimeout) { + scoped_ptr<DeterministicSocketData> socket_data( + new DeterministicSocketData(NULL, 0, NULL, 0)); + socket_data->set_connect_data( + MockConnect(ASYNC, ERR_CONNECTION_TIMED_OUT)); + CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(), + "http://localhost/", socket_data.Pass()); + RunUntilIdle(); + EXPECT_EQ(1006, error()); +} + +// Cancellation during connect works. +TEST_F(WebSocketStreamCreateTest, CancellationDuringConnect) { + scoped_ptr<DeterministicSocketData> socket_data( + new DeterministicSocketData(NULL, 0, NULL, 0)); + socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); + CreateAndConnectRawExpectations("ws://localhost/", + NoSubProtocols(), + "http://localhost/", + socket_data.Pass()); + stream_request_.reset(); + RunUntilIdle(); + EXPECT_FALSE(stream_); +} + +// Cancellation during write of the request headers works. +TEST_F(WebSocketStreamCreateTest, CancellationDuringWrite) { + // We seem to need at least two operations in order to use SetStop(). + MockWrite writes[] = {MockWrite(ASYNC, 0, "GET / HTTP/"), + MockWrite(ASYNC, 1, "1.1\r\n")}; + // We keep a copy of the pointer so that we can call RunFor() on it later. + DeterministicSocketData* socket_data( + new DeterministicSocketData(NULL, 0, writes, arraysize(writes))); + socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); + socket_data->SetStop(1); + CreateAndConnectRawExpectations("ws://localhost/", + NoSubProtocols(), + "http://localhost/", + make_scoped_ptr(socket_data)); + socket_data->Run(); + stream_request_.reset(); + RunUntilIdle(); + EXPECT_FALSE(stream_); +} + +// Cancellation during read of the response headers works. +TEST_F(WebSocketStreamCreateTest, CancellationDuringRead) { + std::string request = WebSocketStandardRequest("/", "http://localhost/", ""); + MockWrite writes[] = {MockWrite(ASYNC, 0, request.c_str())}; + MockRead reads[] = { + MockRead(ASYNC, 1, "HTTP/1.1 101 Switching Protocols\r\nUpgr"), + }; + DeterministicSocketData* socket_data(new DeterministicSocketData( + reads, arraysize(reads), writes, arraysize(writes))); + socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); + socket_data->SetStop(1); + CreateAndConnectRawExpectations("ws://localhost/", + NoSubProtocols(), + "http://localhost/", + make_scoped_ptr(socket_data)); + socket_data->Run(); + stream_request_.reset(); + RunUntilIdle(); + EXPECT_FALSE(stream_); +} + +} // namespace +} // namespace net diff --git a/net/websockets/websocket_test_util.cc b/net/websockets/websocket_test_util.cc index 350a696..55113c6 100644 --- a/net/websockets/websocket_test_util.cc +++ b/net/websockets/websocket_test_util.cc @@ -5,6 +5,8 @@ #include "net/websockets/websocket_test_util.h" #include "base/basictypes.h" +#include "base/strings/stringprintf.h" +#include "net/socket/socket_test_util.h" namespace net { @@ -25,4 +27,99 @@ uint32 LinearCongruentialGenerator::Generate() { return static_cast<uint32>(result >> 16); } +std::string WebSocketStandardRequest(const std::string& path, + const std::string& origin, + const std::string& extra_headers) { + // Unrelated changes in net/http may change the order and default-values of + // HTTP headers, causing WebSocket tests to fail. It is safe to update this + // string in that case. + return base::StringPrintf( + "GET %s HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Origin: %s\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" + "%s\r\n", + path.c_str(), + origin.c_str(), + extra_headers.c_str()); +} + +std::string WebSocketStandardResponse(const std::string& extra_headers) { + return base::StringPrintf( + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + "%s\r\n", + extra_headers.c_str()); +} + +struct WebSocketDeterministicMockClientSocketFactoryMaker::Detail { + std::string expect_written; + std::string return_to_read; + MockRead read; + MockWrite write; + scoped_ptr<DeterministicSocketData> data; + DeterministicMockClientSocketFactory factory; +}; + +WebSocketDeterministicMockClientSocketFactoryMaker:: + WebSocketDeterministicMockClientSocketFactoryMaker() + : detail_(new Detail) {} + +WebSocketDeterministicMockClientSocketFactoryMaker:: + ~WebSocketDeterministicMockClientSocketFactoryMaker() {} + +DeterministicMockClientSocketFactory* +WebSocketDeterministicMockClientSocketFactoryMaker::factory() { + return &detail_->factory; +} + +void WebSocketDeterministicMockClientSocketFactoryMaker::SetExpectations( + const std::string& expect_written, + const std::string& return_to_read) { + // We need to extend the lifetime of these strings. + detail_->expect_written = expect_written; + detail_->return_to_read = return_to_read; + detail_->write = MockWrite(SYNCHRONOUS, 0, detail_->expect_written.c_str()); + detail_->read = MockRead(SYNCHRONOUS, 1, detail_->return_to_read.c_str()); + scoped_ptr<DeterministicSocketData> socket_data( + new DeterministicSocketData(&detail_->read, 1, &detail_->write, 1)); + socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); + socket_data->SetStop(2); + SetRawExpectations(socket_data.Pass()); +} + +void WebSocketDeterministicMockClientSocketFactoryMaker::SetRawExpectations( + scoped_ptr<DeterministicSocketData> socket_data) { + detail_->data = socket_data.Pass(); + detail_->factory.AddSocketDataProvider(detail_->data.get()); +} + +WebSocketTestURLRequestContextHost::WebSocketTestURLRequestContextHost() + : url_request_context_(true) { + url_request_context_.set_client_socket_factory(maker_.factory()); +} + +WebSocketTestURLRequestContextHost::~WebSocketTestURLRequestContextHost() {} + +void WebSocketTestURLRequestContextHost::SetRawExpectations( + scoped_ptr<DeterministicSocketData> socket_data) { + maker_.SetRawExpectations(socket_data.Pass()); +} + +TestURLRequestContext* +WebSocketTestURLRequestContextHost::GetURLRequestContext() { + url_request_context_.Init(); + // A Network Delegate is required to make the URLRequest::Delegate work. + url_request_context_.set_network_delegate(&network_delegate_); + return &url_request_context_; +} + } // namespace net diff --git a/net/websockets/websocket_test_util.h b/net/websockets/websocket_test_util.h index 86ce32b..71b2ce6 100644 --- a/net/websockets/websocket_test_util.h +++ b/net/websockets/websocket_test_util.h @@ -5,8 +5,11 @@ #ifndef NET_WEBSOCKETS_WEBSOCKET_TEST_UTIL_H_ #define NET_WEBSOCKETS_WEBSOCKET_TEST_UTIL_H_ +#include <string> + #include "base/basictypes.h" #include "base/memory/scoped_ptr.h" +#include "net/url_request/url_request_test_util.h" #include "net/websockets/websocket_stream.h" class GURL; @@ -14,8 +17,10 @@ class GURL; namespace net { class BoundNetLog; +class DeterministicSocketData; class URLRequestContext; class WebSocketHandshakeStreamCreateHelper; +class DeterministicMockClientSocketFactory; class LinearCongruentialGenerator { public: @@ -38,6 +43,72 @@ NET_EXPORT_PRIVATE extern scoped_ptr<WebSocketStreamRequest> const BoundNetLog& net_log, scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate); +// Generates a standard WebSocket handshake request. The challenge key used is +// "dGhlIHNhbXBsZSBub25jZQ==". Each header in |extra_headers| must be terminated +// with "\r\n". +extern std::string WebSocketStandardRequest(const std::string& path, + const std::string& origin, + const std::string& extra_headers); + +// A response with the appropriate accept header to match the above challenge +// key. Each header in |extra_headers| must be terminated with "\r\n". +extern std::string WebSocketStandardResponse(const std::string& extra_headers); + +// This class provides a convenient way to construct a +// DeterministicMockClientSocketFactory for WebSocket tests. +class WebSocketDeterministicMockClientSocketFactoryMaker { + public: + WebSocketDeterministicMockClientSocketFactoryMaker(); + ~WebSocketDeterministicMockClientSocketFactoryMaker(); + + // The socket created by the factory will expect |expect_written| to be + // written to the socket, and will respond with |return_to_read|. The test + // will fail if the expected text is not written, or all the bytes are not + // read. + void SetExpectations(const std::string& expect_written, + const std::string& return_to_read); + + // A low-level interface to permit arbitrary expectations to be set. + void SetRawExpectations(scoped_ptr<DeterministicSocketData> socket_data); + + // Call to get a pointer to the factory, which remains owned by this object. + DeterministicMockClientSocketFactory* factory(); + + private: + struct Detail; + scoped_ptr<Detail> detail_; + + DISALLOW_COPY_AND_ASSIGN(WebSocketDeterministicMockClientSocketFactoryMaker); +}; + +// This class encapsulates the details of creating a +// TestURLRequestContext that returns mock ClientSocketHandles that do what is +// required by the tests. +struct WebSocketTestURLRequestContextHost { + public: + WebSocketTestURLRequestContextHost(); + ~WebSocketTestURLRequestContextHost(); + + void SetExpectations(const std::string& expect_written, + const std::string& return_to_read) { + maker_.SetExpectations(expect_written, return_to_read); + } + + void SetRawExpectations(scoped_ptr<DeterministicSocketData> socket_data); + + // Call after calling one of SetExpections() or SetRawExpectations(). The + // returned pointer remains owned by this object. This should only be called + // once. + TestURLRequestContext* GetURLRequestContext(); + + private: + WebSocketDeterministicMockClientSocketFactoryMaker maker_; + TestURLRequestContext url_request_context_; + TestNetworkDelegate network_delegate_; + + DISALLOW_COPY_AND_ASSIGN(WebSocketTestURLRequestContextHost); +}; + } // namespace net #endif // NET_WEBSOCKETS_WEBSOCKET_TEST_UTIL_H_ |