summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--net/net.gyp2
-rw-r--r--net/websockets/README2
-rw-r--r--net/websockets/websocket_basic_handshake_stream.cc13
-rw-r--r--net/websockets/websocket_basic_handshake_stream.h9
-rw-r--r--net/websockets/websocket_handshake_stream_create_helper_test.cc145
-rw-r--r--net/websockets/websocket_stream.h2
-rw-r--r--net/websockets/websocket_stream_test.cc515
-rw-r--r--net/websockets/websocket_test_util.cc97
-rw-r--r--net/websockets/websocket_test_util.h71
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_