// 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