From 433bdabf0f4d673b8ce49250849e8571bd93cf1b Mon Sep 17 00:00:00 2001 From: ricea Date: Sun, 25 Jan 2015 23:25:37 -0800 Subject: [PATCH] Add end to end tests for WebSockets in net/websockets/ Some things (notably proxies) are impossible to test using layout tests, and expensive and difficult to test using browser_tests. Add a way to do simple end-to-end tests in net_unittests. BUG=441709 TEST=net_unittests Review URL: https://codereview.chromium.org/722343003 Cr-Commit-Position: refs/heads/master@{#313045} --- net/BUILD.gn | 1 + net/net.gypi | 1 + net/websockets/websocket_end_to_end_test.cc | 370 ++++++++++++++++++++++++++++ 3 files changed, 372 insertions(+) create mode 100644 net/websockets/websocket_end_to_end_test.cc diff --git a/net/BUILD.gn b/net/BUILD.gn index f235c3010b08..52444d59ea4a 100644 --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -1262,6 +1262,7 @@ if (!is_android && !is_win && !is_mac) { "websockets/websocket_deflate_stream_test.cc", "websockets/websocket_deflater_test.cc", "websockets/websocket_errors_test.cc", + "websockets/websocket_end_to_end_test.cc", "websockets/websocket_extension_parser_test.cc", "websockets/websocket_frame_parser_test.cc", "websockets/websocket_frame_test.cc", diff --git a/net/net.gypi b/net/net.gypi index 6da22e3b423d..5b5a5f9ceca5 100644 --- a/net/net.gypi +++ b/net/net.gypi @@ -1718,6 +1718,7 @@ 'websockets/websocket_deflate_predictor_impl_test.cc', 'websockets/websocket_deflate_stream_test.cc', 'websockets/websocket_deflater_test.cc', + 'websockets/websocket_end_to_end_test.cc', 'websockets/websocket_errors_test.cc', 'websockets/websocket_extension_parser_test.cc', 'websockets/websocket_frame_parser_test.cc', diff --git a/net/websockets/websocket_end_to_end_test.cc b/net/websockets/websocket_end_to_end_test.cc new file mode 100644 index 000000000000..0b5944f33f2c --- /dev/null +++ b/net/websockets/websocket_end_to_end_test.cc @@ -0,0 +1,370 @@ +// Copyright 2014 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. + +// End-to-end tests for WebSocket. +// +// A python server is (re)started for each test, which is moderately +// inefficient. However, it makes these tests a good fit for scenarios which +// require special server configurations. + +#include + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "net/base/auth.h" +#include "net/base/network_delegate.h" +#include "net/base/test_data_directory.h" +#include "net/proxy/proxy_service.h" +#include "net/test/spawned_test_server/spawned_test_server.h" +#include "net/url_request/url_request_test_util.h" +#include "net/websockets/websocket_channel.h" +#include "net/websockets/websocket_event_interface.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/origin.h" + +namespace net { + +namespace { + +static const char kEchoServer[] = "echo-with-no-extension"; + +// An implementation of WebSocketEventInterface that waits for and records the +// results of the connect. +class ConnectTestingEventInterface : public WebSocketEventInterface { + public: + ConnectTestingEventInterface(); + + void WaitForResponse(); + + bool failed() const { return failed_; } + + // Only set if the handshake failed, otherwise empty. + std::string failure_message() const; + + std::string selected_subprotocol() const; + + std::string extensions() const; + + // Implementation of WebSocketEventInterface. + ChannelState OnAddChannelResponse(bool fail, + const std::string& selected_subprotocol, + const std::string& extensions) override; + + ChannelState OnDataFrame(bool fin, + WebSocketMessageType type, + const std::vector& data) override; + + ChannelState OnFlowControl(int64 quota) override; + + ChannelState OnClosingHandshake() override; + + ChannelState OnDropChannel(bool was_clean, + uint16 code, + const std::string& reason) override; + + ChannelState OnFailChannel(const std::string& message) override; + + ChannelState OnStartOpeningHandshake( + scoped_ptr request) override; + + ChannelState OnFinishOpeningHandshake( + scoped_ptr response) override; + + ChannelState OnSSLCertificateError( + scoped_ptr ssl_error_callbacks, + const GURL& url, + const SSLInfo& ssl_info, + bool fatal) override; + + private: + void QuitNestedEventLoop(); + + // failed_ is true if the handshake failed (ie. OnFailChannel was called). + bool failed_; + std::string selected_subprotocol_; + std::string extensions_; + std::string failure_message_; + base::RunLoop run_loop_; + + DISALLOW_COPY_AND_ASSIGN(ConnectTestingEventInterface); +}; + +ConnectTestingEventInterface::ConnectTestingEventInterface() : failed_(true) { +} + +void ConnectTestingEventInterface::WaitForResponse() { + run_loop_.Run(); +} + +std::string ConnectTestingEventInterface::failure_message() const { + return failure_message_; +} + +std::string ConnectTestingEventInterface::selected_subprotocol() const { + return selected_subprotocol_; +} + +std::string ConnectTestingEventInterface::extensions() const { + return extensions_; +} + +// Make the function definitions below less verbose. +typedef ConnectTestingEventInterface::ChannelState ChannelState; + +ChannelState ConnectTestingEventInterface::OnAddChannelResponse( + bool fail, + const std::string& selected_subprotocol, + const std::string& extensions) { + failed_ = fail; + selected_subprotocol_ = selected_subprotocol; + extensions_ = extensions; + QuitNestedEventLoop(); + return fail ? CHANNEL_DELETED : CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnDataFrame( + bool fin, + WebSocketMessageType type, + const std::vector& data) { + return CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnFlowControl(int64 quota) { + return CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnClosingHandshake() { + return CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnDropChannel( + bool was_clean, + uint16 code, + const std::string& reason) { + return CHANNEL_DELETED; +} + +ChannelState ConnectTestingEventInterface::OnFailChannel( + const std::string& message) { + failed_ = true; + failure_message_ = message; + QuitNestedEventLoop(); + return CHANNEL_DELETED; +} + +ChannelState ConnectTestingEventInterface::OnStartOpeningHandshake( + scoped_ptr request) { + return CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnFinishOpeningHandshake( + scoped_ptr response) { + return CHANNEL_ALIVE; +} + +ChannelState ConnectTestingEventInterface::OnSSLCertificateError( + scoped_ptr ssl_error_callbacks, + const GURL& url, + const SSLInfo& ssl_info, + bool fatal) { + base::MessageLoop::current()->PostTask( + FROM_HERE, base::Bind(&SSLErrorCallbacks::CancelSSLRequest, + base::Owned(ssl_error_callbacks.release()), + ERR_SSL_PROTOCOL_ERROR, &ssl_info)); + return CHANNEL_ALIVE; +} + +void ConnectTestingEventInterface::QuitNestedEventLoop() { + run_loop_.Quit(); +} + +// A subclass of TestNetworkDelegate that additionally implements the +// OnResolveProxy callback and records the information passed to it. +class TestNetworkDelegateWithProxyInfo : public TestNetworkDelegate { + public: + TestNetworkDelegateWithProxyInfo() {} + + struct ResolvedProxyInfo { + GURL url; + ProxyInfo proxy_info; + }; + + const ResolvedProxyInfo& resolved_proxy_info() const { + return resolved_proxy_info_; + } + + protected: + void OnResolveProxy(const GURL& url, + int load_flags, + const ProxyService& proxy_service, + ProxyInfo* result) override { + resolved_proxy_info_.url = url; + resolved_proxy_info_.proxy_info = *result; + } + + private: + ResolvedProxyInfo resolved_proxy_info_; + + DISALLOW_COPY_AND_ASSIGN(TestNetworkDelegateWithProxyInfo); +}; + +class WebSocketEndToEndTest : public ::testing::Test { + protected: + WebSocketEndToEndTest() + : event_interface_(new ConnectTestingEventInterface), + network_delegate_(new TestNetworkDelegateWithProxyInfo), + context_(true), + channel_(make_scoped_ptr(event_interface_), &context_), + initialised_context_(false) {} + + // Initialise the URLRequestContext. Normally done automatically by + // ConnectAndWait(). This method is for the use of tests that need the + // URLRequestContext initialised before calling ConnectAndWait(). + void InitialiseContext() { + context_.set_network_delegate(network_delegate_.get()); + context_.Init(); + initialised_context_ = true; + } + + // Send the connect request to |socket_url| and wait for a response. Returns + // true if the handshake succeeded. + bool ConnectAndWait(const GURL& socket_url) { + if (!initialised_context_) { + InitialiseContext(); + } + std::vector sub_protocols; + url::Origin origin("http://localhost"); + channel_.SendAddChannelRequest(GURL(socket_url), sub_protocols, origin); + event_interface_->WaitForResponse(); + return !event_interface_->failed(); + } + + ConnectTestingEventInterface* event_interface_; // owned by channel_ + scoped_ptr network_delegate_; + TestURLRequestContext context_; + WebSocketChannel channel_; + bool initialised_context_; +}; + +// None of these tests work on Android. +// TODO(ricea): Make these tests work on Android. See crbug.com/441711. +#if defined(OS_ANDROID) +#define DISABLED_ON_ANDROID(test) DISABLED_##test +#else +#define DISABLED_ON_ANDROID(test) test +#endif + +// Basic test of connectivity. If this test fails, nothing else can be expected +// to work. +TEST_F(WebSocketEndToEndTest, DISABLED_ON_ANDROID(BasicSmokeTest)) { + SpawnedTestServer ws_server(SpawnedTestServer::TYPE_WS, + SpawnedTestServer::kLocalhost, + GetWebSocketTestDataDirectory()); + ASSERT_TRUE(ws_server.Start()); + EXPECT_TRUE(ConnectAndWait(ws_server.GetURL(kEchoServer))); +} + +// Test for issue crbug.com/433695 "Unencrypted WebSocket connection via +// authenticated proxy times out" +// TODO(ricea): Enable this when the issue is fixed. +TEST_F(WebSocketEndToEndTest, DISABLED_HttpsProxyUnauthedFails) { + SpawnedTestServer proxy_server(SpawnedTestServer::TYPE_BASIC_AUTH_PROXY, + SpawnedTestServer::kLocalhost, + base::FilePath()); + SpawnedTestServer ws_server(SpawnedTestServer::TYPE_WS, + SpawnedTestServer::kLocalhost, + GetWebSocketTestDataDirectory()); + ASSERT_TRUE(proxy_server.StartInBackground()); + ASSERT_TRUE(ws_server.StartInBackground()); + ASSERT_TRUE(proxy_server.BlockUntilStarted()); + ASSERT_TRUE(ws_server.BlockUntilStarted()); + std::string proxy_config = + "https=" + proxy_server.host_port_pair().ToString(); + scoped_ptr proxy_service( + ProxyService::CreateFixed(proxy_config)); + ASSERT_TRUE(proxy_service); + context_.set_proxy_service(proxy_service.get()); + EXPECT_FALSE(ConnectAndWait(ws_server.GetURL(kEchoServer))); + EXPECT_EQ("Proxy authentication failed", event_interface_->failure_message()); +} + +TEST_F(WebSocketEndToEndTest, DISABLED_ON_ANDROID(HttpsWssProxyUnauthedFails)) { + SpawnedTestServer proxy_server(SpawnedTestServer::TYPE_BASIC_AUTH_PROXY, + SpawnedTestServer::kLocalhost, + base::FilePath()); + SpawnedTestServer wss_server(SpawnedTestServer::TYPE_WSS, + SpawnedTestServer::kLocalhost, + GetWebSocketTestDataDirectory()); + ASSERT_TRUE(proxy_server.StartInBackground()); + ASSERT_TRUE(wss_server.StartInBackground()); + ASSERT_TRUE(proxy_server.BlockUntilStarted()); + ASSERT_TRUE(wss_server.BlockUntilStarted()); + std::string proxy_config = + "https=" + proxy_server.host_port_pair().ToString(); + scoped_ptr proxy_service( + ProxyService::CreateFixed(proxy_config)); + ASSERT_TRUE(proxy_service); + context_.set_proxy_service(proxy_service.get()); + EXPECT_FALSE(ConnectAndWait(wss_server.GetURL(kEchoServer))); + EXPECT_EQ("Proxy authentication failed", event_interface_->failure_message()); +} + +// Regression test for crbug/426736 "WebSocket connections not using configured +// system HTTPS Proxy". +TEST_F(WebSocketEndToEndTest, DISABLED_ON_ANDROID(HttpsProxyUsed)) { + SpawnedTestServer proxy_server(SpawnedTestServer::TYPE_BASIC_AUTH_PROXY, + SpawnedTestServer::kLocalhost, + base::FilePath()); + SpawnedTestServer ws_server(SpawnedTestServer::TYPE_WS, + SpawnedTestServer::kLocalhost, + GetWebSocketTestDataDirectory()); + ASSERT_TRUE(proxy_server.StartInBackground()); + ASSERT_TRUE(ws_server.StartInBackground()); + ASSERT_TRUE(proxy_server.BlockUntilStarted()); + ASSERT_TRUE(ws_server.BlockUntilStarted()); + std::string proxy_config = "https=" + + proxy_server.host_port_pair().ToString() + ";" + + "http=" + proxy_server.host_port_pair().ToString(); + scoped_ptr proxy_service( + ProxyService::CreateFixed(proxy_config)); + context_.set_proxy_service(proxy_service.get()); + InitialiseContext(); + + // The test server doesn't have an unauthenticated proxy mode. WebSockets + // cannot provide auth information that isn't already cached, so it's + // necessary to preflight an HTTP request to authenticate against the proxy. + std::string scheme("http"); + GURL::Replacements replacements; + replacements.SetSchemeStr(scheme); + // It doesn't matter what the URL is, as long as it is an HTTP navigation. + GURL http_page = + ws_server.GetURL("connect_check.html").ReplaceComponents(replacements); + TestDelegate delegate; + delegate.set_credentials( + AuthCredentials(base::ASCIIToUTF16("foo"), base::ASCIIToUTF16("bar"))); + { + scoped_ptr request( + context_.CreateRequest(http_page, DEFAULT_PRIORITY, &delegate, NULL)); + request->Start(); + // TestDelegate exits the message loop when the request completes by + // default. + base::RunLoop().Run(); + EXPECT_TRUE(delegate.auth_required_called()); + } + + GURL ws_url = ws_server.GetURL(kEchoServer); + EXPECT_TRUE(ConnectAndWait(ws_url)); + const TestNetworkDelegateWithProxyInfo::ResolvedProxyInfo& info = + network_delegate_->resolved_proxy_info(); + EXPECT_EQ(ws_url, info.url); + EXPECT_TRUE(info.proxy_info.is_http()); +} + +} // namespace + +} // namespace net -- 2.11.4.GIT