// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/websockets/websocket_stream.h" #include #include #include #include #include "base/compiler_specific.h" #include "base/containers/span.h" #include "base/memory/ptr_util.h" #include "base/metrics/histogram.h" #include "base/metrics/histogram_samples.h" #include "base/metrics/statistics_recorder.h" #include "base/run_loop.h" #include "base/strings/string_piece.h" #include "base/strings/stringprintf.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "base/timer/mock_timer.h" #include "base/timer/timer.h" #include "net/base/features.h" #include "net/base/isolation_info.h" #include "net/base/net_errors.h" #include "net/base/url_util.h" #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/proxy_resolution/proxy_resolution_service.h" #include "net/socket/client_socket_handle.h" #include "net/socket/socket_test_util.h" #include "net/spdy/spdy_test_util_common.h" #include "net/test/cert_test_util.h" #include "net/test/gtest_util.h" #include "net/test/test_data_directory.h" #include "net/third_party/quiche/src/quiche/spdy/core/spdy_protocol.h" #include "net/url_request/url_request_test_util.h" #include "net/websockets/websocket_basic_handshake_stream.h" #include "net/websockets/websocket_frame.h" #include "net/websockets/websocket_stream_create_test_base.h" #include "net/websockets/websocket_test_util.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "url/gurl.h" #include "url/origin.h" using ::net::test::IsError; using ::net::test::IsOk; using ::testing::TestWithParam; using ::testing::Values; namespace net { namespace { enum HandshakeStreamType { BASIC_HANDSHAKE_STREAM, HTTP2_HANDSHAKE_STREAM }; // Simple builder for a SequencedSocketData object to save repetitive code. // It always sets the connect data to MockConnect(SYNCHRONOUS, OK), so it cannot // be used in tests where the connect fails. In practice, those tests never have // any read/write data and so can't benefit from it anyway. The arrays are not // copied. It is up to the caller to ensure they stay in scope until the test // ends. std::unique_ptr BuildSocketData( base::span reads, base::span writes) { auto socket_data = std::make_unique(reads, writes); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); return socket_data; } // Builder for a SequencedSocketData that expects nothing. This does not // set the connect data, so the calling code must do that explicitly. std::unique_ptr BuildNullSocketData() { return std::make_unique(); } class MockWeakTimer : public base::MockOneShotTimer, public base::SupportsWeakPtr { public: MockWeakTimer() = default; }; const char kOrigin[] = "http://www.example.org"; static url::Origin Origin() { return url::Origin::Create(GURL(kOrigin)); } static net::SiteForCookies SiteForCookies() { return net::SiteForCookies::FromOrigin(Origin()); } static IsolationInfo CreateIsolationInfo() { url::Origin origin = Origin(); return IsolationInfo::Create(IsolationInfo::RequestType::kOther, origin, origin, SiteForCookies::FromOrigin(origin)); } class WebSocketStreamCreateTest : public TestWithParam, public WebSocketStreamCreateTestBase { protected: WebSocketStreamCreateTest() : stream_type_(GetParam()) { // Make sure these tests all pass with connection partitioning enabled. The // disabled case is less interesting, and is tested more directly at lower // layers. feature_list_.InitAndEnableFeature( features::kPartitionConnectionsByNetworkIsolationKey); } ~WebSocketStreamCreateTest() override { // Permit any endpoint locks to be released. stream_request_.reset(); stream_.reset(); base::RunLoop().RunUntilIdle(); } // Normally it's easier to use CreateAndConnectRawExpectations() instead. This // method is only needed when multiple sockets are involved. void AddRawExpectations(std::unique_ptr socket_data) { url_request_context_host_.AddRawExpectations(std::move(socket_data)); } void AddSSLData() { auto ssl_data = std::make_unique(ASYNC, OK); ssl_data->ssl_info.cert = ImportCertFromFile(GetTestCertsDirectory(), "wildcard.pem"); if (stream_type_ == HTTP2_HANDSHAKE_STREAM) ssl_data->next_proto = kProtoHTTP2; ASSERT_TRUE(ssl_data->ssl_info.cert.get()); url_request_context_host_.AddSSLSocketDataProvider(std::move(ssl_data)); } void SetTimer(std::unique_ptr timer) { timer_ = std::move(timer); } void SetAdditionalResponseData(std::string additional_data) { additional_data_ = std::move(additional_data); } void SetHttp2ResponseStatus(const char* const http2_response_status) { http2_response_status_ = http2_response_status; } void SetResetWebSocketHttp2Stream(bool reset_websocket_http2_stream) { reset_websocket_http2_stream_ = reset_websocket_http2_stream; } // Set up mock data and start websockets request, either for WebSocket // upgraded from an HTTP/1 connection, or for a WebSocket request over HTTP/2. void CreateAndConnectStandard( base::StringPiece url, const std::vector& sub_protocols, const WebSocketExtraHeaders& send_additional_request_headers, const WebSocketExtraHeaders& extra_request_headers, const WebSocketExtraHeaders& extra_response_headers) { const GURL socket_url(url); const std::string socket_host = GetHostAndOptionalPort(socket_url); const std::string socket_path = socket_url.path(); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { url_request_context_host_.SetExpectations( WebSocketStandardRequest(socket_path, socket_host, Origin(), send_additional_request_headers, extra_request_headers), WebSocketStandardResponse( WebSocketExtraHeadersToString(extra_response_headers)) + additional_data_); CreateAndConnectStream(socket_url, sub_protocols, Origin(), SiteForCookies(), CreateIsolationInfo(), WebSocketExtraHeadersToHttpRequestHeaders( send_additional_request_headers), std::move(timer_)); return; } DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); // TODO(bnc): Find a way to clear // spdy_session_pool.enable_sending_initial_data_ to avoid sending // connection preface, initial settings, and window update. // HTTP/2 connection preface. frames_.emplace_back(const_cast(spdy::kHttp2ConnectionHeaderPrefix), spdy::kHttp2ConnectionHeaderPrefixSize, /* owns_buffer = */ false); AddWrite(&frames_.back()); // Server advertises WebSockets over HTTP/2 support. spdy::SettingsMap read_settings; read_settings[spdy::SETTINGS_ENABLE_CONNECT_PROTOCOL] = 1; frames_.push_back(spdy_util_.ConstructSpdySettings(read_settings)); AddRead(&frames_.back()); // Initial SETTINGS frame. spdy::SettingsMap write_settings; write_settings[spdy::SETTINGS_HEADER_TABLE_SIZE] = kSpdyMaxHeaderTableSize; write_settings[spdy::SETTINGS_MAX_CONCURRENT_STREAMS] = kSpdyMaxConcurrentPushedStreams; write_settings[spdy::SETTINGS_INITIAL_WINDOW_SIZE] = 6 * 1024 * 1024; write_settings[spdy::SETTINGS_MAX_HEADER_LIST_SIZE] = kSpdyMaxHeaderListSize; frames_.push_back(spdy_util_.ConstructSpdySettings(write_settings)); AddWrite(&frames_.back()); // Initial window update frame. frames_.push_back(spdy_util_.ConstructSpdyWindowUpdate(0, 0x00ef0001)); AddWrite(&frames_.back()); // SETTINGS ACK sent as a response to server's SETTINGS frame. frames_.push_back(spdy_util_.ConstructSpdySettingsAck()); AddWrite(&frames_.back()); // First request. This is necessary, because a WebSockets request currently // does not open a new HTTP/2 connection, it only uses an existing one. const char* const kExtraRequestHeaders[] = { "user-agent", "", "accept-encoding", "gzip, deflate", "accept-language", "en-us,fr"}; frames_.push_back(spdy_util_.ConstructSpdyGet( kExtraRequestHeaders, std::size(kExtraRequestHeaders) / 2, 1, DEFAULT_PRIORITY)); AddWrite(&frames_.back()); // SETTINGS ACK frame sent by the server in response to the client's // initial SETTINGS frame. frames_.push_back(spdy_util_.ConstructSpdySettingsAck()); AddRead(&frames_.back()); // Response headers to first request. frames_.push_back(spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1)); AddRead(&frames_.back()); // Response body to first request. frames_.push_back(spdy_util_.ConstructSpdyDataFrame(1, true)); AddRead(&frames_.back()); // First request is closed. spdy_util_.UpdateWithStreamDestruction(1); // WebSocket request. spdy::Http2HeaderBlock request_headers = WebSocketHttp2Request( socket_path, socket_host, kOrigin, extra_request_headers); frames_.push_back(spdy_util_.ConstructSpdyHeaders( 3, std::move(request_headers), DEFAULT_PRIORITY, false)); AddWrite(&frames_.back()); if (reset_websocket_http2_stream_) { frames_.push_back( spdy_util_.ConstructSpdyRstStream(3, spdy::ERROR_CODE_CANCEL)); AddRead(&frames_.back()); } else { // Response to WebSocket request. std::vector extra_response_header_keys; std::vector extra_response_headers_vector; for (const auto& extra_header : extra_response_headers) { // Save a lowercase copy of the header key. extra_response_header_keys.push_back( base::ToLowerASCII(extra_header.first)); // Save a pointer to this lowercase copy. extra_response_headers_vector.push_back( extra_response_header_keys.back().c_str()); // Save a pointer to the original header value provided by the caller. extra_response_headers_vector.push_back(extra_header.second.c_str()); } frames_.push_back(spdy_util_.ConstructSpdyReplyError( http2_response_status_, extra_response_headers_vector.data(), extra_response_headers_vector.size() / 2, 3)); AddRead(&frames_.back()); // WebSocket data received. if (!additional_data_.empty()) { frames_.push_back( spdy_util_.ConstructSpdyDataFrame(3, additional_data_, true)); AddRead(&frames_.back()); } // Client cancels HTTP/2 stream when request is destroyed. frames_.push_back( spdy_util_.ConstructSpdyRstStream(3, spdy::ERROR_CODE_CANCEL)); AddWrite(&frames_.back()); } // EOF. reads_.emplace_back(ASYNC, 0, sequence_number_++); auto socket_data = std::make_unique(reads_, writes_); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); AddRawExpectations(std::move(socket_data)); // Send first request. This makes sure server's // spdy::SETTINGS_ENABLE_CONNECT_PROTOCOL advertisement is read. URLRequestContext* context = url_request_context_host_.GetURLRequestContext(); TestDelegate delegate; std::unique_ptr request = context->CreateRequest( GURL("https://www.example.org/"), DEFAULT_PRIORITY, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS, /*is_for_websockets=*/false); // The IsolationInfo has to match for a socket to be reused. request->set_isolation_info(CreateIsolationInfo()); request->Start(); EXPECT_TRUE(request->is_pending()); delegate.RunUntilComplete(); EXPECT_FALSE(request->is_pending()); CreateAndConnectStream(socket_url, sub_protocols, Origin(), SiteForCookies(), CreateIsolationInfo(), WebSocketExtraHeadersToHttpRequestHeaders( send_additional_request_headers), std::move(timer_)); } // Like CreateAndConnectStandard(), but allow for arbitrary response body. // Only for HTTP/1-based WebSockets. void CreateAndConnectCustomResponse( base::StringPiece url, const std::vector& sub_protocols, const WebSocketExtraHeaders& send_additional_request_headers, const WebSocketExtraHeaders& extra_request_headers, const std::string& response_body) { ASSERT_EQ(BASIC_HANDSHAKE_STREAM, stream_type_); const GURL socket_url(url); const std::string socket_host = GetHostAndOptionalPort(socket_url); const std::string socket_path = socket_url.path(); url_request_context_host_.SetExpectations( WebSocketStandardRequest(socket_path, socket_host, Origin(), send_additional_request_headers, extra_request_headers), response_body); CreateAndConnectStream(socket_url, sub_protocols, Origin(), SiteForCookies(), CreateIsolationInfo(), WebSocketExtraHeadersToHttpRequestHeaders( send_additional_request_headers), nullptr); } // Like CreateAndConnectStandard(), but take extra response headers as a // string. This can save space in case of a very large response. // Only for HTTP/1-based WebSockets. void CreateAndConnectStringResponse( base::StringPiece url, const std::vector& sub_protocols, const std::string& extra_response_headers) { ASSERT_EQ(BASIC_HANDSHAKE_STREAM, stream_type_); const GURL socket_url(url); const std::string socket_host = GetHostAndOptionalPort(socket_url); const std::string socket_path = socket_url.path(); url_request_context_host_.SetExpectations( WebSocketStandardRequest(socket_path, socket_host, Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}), WebSocketStandardResponse(extra_response_headers)); CreateAndConnectStream(socket_url, sub_protocols, Origin(), SiteForCookies(), CreateIsolationInfo(), HttpRequestHeaders(), nullptr); } // Like CreateAndConnectStandard(), but take raw mock data. void CreateAndConnectRawExpectations( base::StringPiece url, const std::vector& sub_protocols, const HttpRequestHeaders& additional_headers, std::unique_ptr socket_data) { ASSERT_EQ(BASIC_HANDSHAKE_STREAM, stream_type_); AddRawExpectations(std::move(socket_data)); CreateAndConnectStream(GURL(url), sub_protocols, Origin(), SiteForCookies(), CreateIsolationInfo(), additional_headers, std::move(timer_)); } private: void AddWrite(const spdy::SpdySerializedFrame* frame) { writes_.emplace_back(ASYNC, frame->data(), frame->size(), sequence_number_++); } void AddRead(const spdy::SpdySerializedFrame* frame) { reads_.emplace_back(ASYNC, frame->data(), frame->size(), sequence_number_++); } protected: const HandshakeStreamType stream_type_; private: base::test::ScopedFeatureList feature_list_; std::unique_ptr timer_; std::string additional_data_; const char* http2_response_status_ = "200"; bool reset_websocket_http2_stream_ = false; SpdyTestUtil spdy_util_; NetLogWithSource log_; int sequence_number_ = 0; // Store mock HTTP/2 data. std::vector frames_; // Store MockRead and MockWrite objects that have pointers to above data. std::vector reads_; std::vector writes_; }; INSTANTIATE_TEST_SUITE_P(All, WebSocketStreamCreateTest, Values(BASIC_HANDSHAKE_STREAM)); using WebSocketMultiProtocolStreamCreateTest = WebSocketStreamCreateTest; INSTANTIATE_TEST_SUITE_P(All, WebSocketMultiProtocolStreamCreateTest, Values(BASIC_HANDSHAKE_STREAM, HTTP2_HANDSHAKE_STREAM)); // There are enough tests of the Sec-WebSocket-Extensions header that they // deserve their own test fixture. class WebSocketStreamCreateExtensionTest : public WebSocketMultiProtocolStreamCreateTest { protected: // Performs a standard connect, with the value of the Sec-WebSocket-Extensions // header in the response set to |extensions_header_value|. Runs the event // loop to allow the connect to complete. void CreateAndConnectWithExtensions( const std::string& extensions_header_value) { AddSSLData(); CreateAndConnectStandard( "wss://www.example.org/testing_path", NoSubProtocols(), {}, {}, {{"Sec-WebSocket-Extensions", extensions_header_value}}); WaitUntilConnectDone(); } }; INSTANTIATE_TEST_SUITE_P(All, WebSocketStreamCreateExtensionTest, Values(BASIC_HANDSHAKE_STREAM, HTTP2_HANDSHAKE_STREAM)); // Common code to construct expectations for authentication tests that receive // the auth challenge on one connection and then create a second connection to // send the authenticated request on. class CommonAuthTestHelper { public: CommonAuthTestHelper() : reads_(), writes_() {} CommonAuthTestHelper(const CommonAuthTestHelper&) = delete; CommonAuthTestHelper& operator=(const CommonAuthTestHelper&) = delete; std::unique_ptr BuildAuthSocketData( std::string response1, std::string request2, std::string response2) { request1_ = WebSocketStandardRequest("/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); response1_ = std::move(response1); request2_ = std::move(request2); response2_ = std::move(response2); writes_[0] = MockWrite(SYNCHRONOUS, 0, request1_.c_str()); reads_[0] = MockRead(SYNCHRONOUS, 1, response1_.c_str()); writes_[1] = MockWrite(SYNCHRONOUS, 2, request2_.c_str()); reads_[1] = MockRead(SYNCHRONOUS, 3, response2_.c_str()); reads_[2] = MockRead(SYNCHRONOUS, OK, 4); // Close connection return BuildSocketData(reads_, writes_); } private: // These need to be object-scoped since they have to remain valid until all // socket operations in the test are complete. std::string request1_; std::string request2_; std::string response1_; std::string response2_; MockRead reads_[3]; MockWrite writes_[2]; }; // Data and methods for BasicAuth tests. class WebSocketStreamCreateBasicAuthTest : public WebSocketStreamCreateTest { protected: void CreateAndConnectAuthHandshake(base::StringPiece url, base::StringPiece base64_user_pass, base::StringPiece response2) { CreateAndConnectRawExpectations( url, NoSubProtocols(), HttpRequestHeaders(), helper_.BuildAuthSocketData(kUnauthorizedResponse, RequestExpectation(base64_user_pass), std::string(response2))); } static std::string RequestExpectation(base::StringPiece base64_user_pass) { static const char request2format[] = "GET / HTTP/1.1\r\n" "Host: www.example.org\r\n" "Connection: Upgrade\r\n" "Pragma: no-cache\r\n" "Cache-Control: no-cache\r\n" "Authorization: Basic %s\r\n" "Upgrade: websocket\r\n" "Origin: http://www.example.org\r\n" "Sec-WebSocket-Version: 13\r\n" "User-Agent: \r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: en-us,fr\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Extensions: permessage-deflate; " "client_max_window_bits\r\n" "\r\n"; return base::StringPrintf(request2format, base64_user_pass.data()); } static const char kUnauthorizedResponse[]; CommonAuthTestHelper helper_; }; INSTANTIATE_TEST_SUITE_P(All, WebSocketStreamCreateBasicAuthTest, Values(BASIC_HANDSHAKE_STREAM)); class WebSocketStreamCreateDigestAuthTest : public WebSocketStreamCreateTest { protected: static const char kUnauthorizedResponse[]; static const char kAuthorizedRequest[]; CommonAuthTestHelper helper_; }; INSTANTIATE_TEST_SUITE_P(All, WebSocketStreamCreateDigestAuthTest, Values(BASIC_HANDSHAKE_STREAM)); const char WebSocketStreamCreateBasicAuthTest::kUnauthorizedResponse[] = "HTTP/1.1 401 Unauthorized\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Basic realm=\"camelot\"\r\n" "\r\n"; // These negotiation values are borrowed from // http_auth_handler_digest_unittest.cc. Feel free to come up with new ones if // you are bored. Only the weakest (no qop) variants of Digest authentication // can be tested by this method, because the others involve random input. const char WebSocketStreamCreateDigestAuthTest::kUnauthorizedResponse[] = "HTTP/1.1 401 Unauthorized\r\n" "Content-Length: 0\r\n" "WWW-Authenticate: Digest realm=\"Oblivion\", nonce=\"nonce-value\"\r\n" "\r\n"; const char WebSocketStreamCreateDigestAuthTest::kAuthorizedRequest[] = "GET / HTTP/1.1\r\n" "Host: www.example.org\r\n" "Connection: Upgrade\r\n" "Pragma: no-cache\r\n" "Cache-Control: no-cache\r\n" "Authorization: Digest username=\"FooBar\", realm=\"Oblivion\", " "nonce=\"nonce-value\", uri=\"/\", " "response=\"f72ff54ebde2f928860f806ec04acd1b\"\r\n" "Upgrade: websocket\r\n" "Origin: http://www.example.org\r\n" "Sec-WebSocket-Version: 13\r\n" "User-Agent: \r\n" "Accept-Encoding: gzip, deflate\r\n" "Accept-Language: en-us,fr\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Extensions: permessage-deflate; " "client_max_window_bits\r\n" "\r\n"; // Confirm that the basic case works as expected. TEST_P(WebSocketMultiProtocolStreamCreateTest, SimpleSuccess) { base::HistogramTester histogram_tester; AddSSLData(); EXPECT_FALSE(url_request_); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); EXPECT_TRUE(url_request_); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); EXPECT_TRUE(request_info_); EXPECT_TRUE(response_info_); EXPECT_EQ(ERR_WS_UPGRADE, url_request_context_host_.network_delegate().last_error()); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::CONNECTED))); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::HTTP2_CONNECTED))); } } TEST_P(WebSocketStreamCreateTest, HandshakeInfo) { static const char kResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "foo: bar, baz\r\n" "hoge: fuga\r\n" "hoge: piyo\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kResponse); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); WaitUntilConnectDone(); EXPECT_TRUE(stream_); ASSERT_TRUE(request_info_); ASSERT_TRUE(response_info_); std::vector request_headers = RequestHeadersToVector(request_info_->headers); // We examine the contents of request_info_ and response_info_ // mainly only in this test case. EXPECT_EQ(GURL("ws://www.example.org/"), request_info_->url); EXPECT_EQ(GURL("ws://www.example.org/"), response_info_->url); EXPECT_EQ(101, response_info_->headers->response_code()); EXPECT_EQ("Switching Protocols", response_info_->headers->GetStatusText()); ASSERT_EQ(12u, request_headers.size()); EXPECT_EQ(HeaderKeyValuePair("Host", "www.example.org"), request_headers[0]); EXPECT_EQ(HeaderKeyValuePair("Connection", "Upgrade"), request_headers[1]); EXPECT_EQ(HeaderKeyValuePair("Pragma", "no-cache"), request_headers[2]); EXPECT_EQ(HeaderKeyValuePair("Cache-Control", "no-cache"), request_headers[3]); EXPECT_EQ(HeaderKeyValuePair("Upgrade", "websocket"), request_headers[4]); EXPECT_EQ(HeaderKeyValuePair("Origin", "http://www.example.org"), request_headers[5]); EXPECT_EQ(HeaderKeyValuePair("Sec-WebSocket-Version", "13"), request_headers[6]); EXPECT_EQ(HeaderKeyValuePair("User-Agent", ""), request_headers[7]); EXPECT_EQ(HeaderKeyValuePair("Accept-Encoding", "gzip, deflate"), request_headers[8]); EXPECT_EQ(HeaderKeyValuePair("Accept-Language", "en-us,fr"), request_headers[9]); EXPECT_EQ("Sec-WebSocket-Key", request_headers[10].first); EXPECT_EQ(HeaderKeyValuePair("Sec-WebSocket-Extensions", "permessage-deflate; client_max_window_bits"), request_headers[11]); std::vector response_headers = ResponseHeadersToVector(*response_info_->headers.get()); ASSERT_EQ(6u, response_headers.size()); // Sort the headers for ease of verification. std::sort(response_headers.begin(), response_headers.end()); EXPECT_EQ(HeaderKeyValuePair("Connection", "Upgrade"), response_headers[0]); EXPECT_EQ("Sec-WebSocket-Accept", response_headers[1].first); EXPECT_EQ(HeaderKeyValuePair("Upgrade", "websocket"), response_headers[2]); EXPECT_EQ(HeaderKeyValuePair("foo", "bar, baz"), response_headers[3]); EXPECT_EQ(HeaderKeyValuePair("hoge", "fuga"), response_headers[4]); EXPECT_EQ(HeaderKeyValuePair("hoge", "piyo"), response_headers[5]); } // Confirms that request headers are overriden/added after handshake TEST_P(WebSocketStreamCreateTest, HandshakeOverrideHeaders) { WebSocketExtraHeaders additional_headers( {{"User-Agent", "OveRrIde"}, {"rAnDomHeader", "foobar"}}); CreateAndConnectStandard("ws://www.example.org/", NoSubProtocols(), additional_headers, additional_headers, {}); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); EXPECT_TRUE(request_info_); EXPECT_TRUE(response_info_); std::vector request_headers = RequestHeadersToVector(request_info_->headers); EXPECT_EQ(HeaderKeyValuePair("User-Agent", "OveRrIde"), request_headers[4]); EXPECT_EQ(HeaderKeyValuePair("rAnDomHeader", "foobar"), request_headers[5]); } // Confirm that the stream isn't established until the message loop runs. TEST_P(WebSocketStreamCreateTest, NeedsToRunLoop) { CreateAndConnectStandard("ws://www.example.org/", NoSubProtocols(), {}, {}, {}); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); } // Check the path is used. TEST_P(WebSocketMultiProtocolStreamCreateTest, PathIsUsed) { AddSSLData(); CreateAndConnectStandard("wss://www.example.org/testing_path", NoSubProtocols(), {}, {}, {}); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // Check that sub-protocols are sent and parsed. TEST_P(WebSocketMultiProtocolStreamCreateTest, SubProtocolIsUsed) { AddSSLData(); std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard( "wss://www.example.org/testing_path", sub_protocols, {}, {{"Sec-WebSocket-Protocol", "chatv11.chromium.org, chatv20.chromium.org"}}, {{"Sec-WebSocket-Protocol", "chatv20.chromium.org"}}); WaitUntilConnectDone(); ASSERT_TRUE(stream_); EXPECT_FALSE(has_failed()); EXPECT_EQ("chatv20.chromium.org", stream_->GetSubProtocol()); } // Unsolicited sub-protocols are rejected. TEST_P(WebSocketMultiProtocolStreamCreateTest, UnsolicitedSubProtocol) { base::HistogramTester histogram_tester; AddSSLData(); CreateAndConnectStandard( "wss://www.example.org/testing_path", NoSubProtocols(), {}, {}, {{"Sec-WebSocket-Protocol", "chatv20.chromium.org"}}); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Response must not include 'Sec-WebSocket-Protocol' header " "if not present in request: chatv20.chromium.org", failure_message()); EXPECT_EQ(ERR_INVALID_RESPONSE, url_request_context_host_.network_delegate().last_error()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED_SUBPROTO))); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult:: HTTP2_FAILED_SUBPROTO))); } } // Missing sub-protocol response is rejected. TEST_P(WebSocketMultiProtocolStreamCreateTest, UnacceptedSubProtocol) { AddSSLData(); std::vector sub_protocols; sub_protocols.push_back("chat.example.com"); CreateAndConnectStandard("wss://www.example.org/testing_path", sub_protocols, {}, {{"Sec-WebSocket-Protocol", "chat.example.com"}}, {}); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Sent non-empty 'Sec-WebSocket-Protocol' header " "but no response was received", failure_message()); } // Only one sub-protocol can be accepted. TEST_P(WebSocketMultiProtocolStreamCreateTest, MultipleSubProtocolsInResponse) { AddSSLData(); std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard("wss://www.example.org/testing_path", sub_protocols, {}, {{"Sec-WebSocket-Protocol", "chatv11.chromium.org, chatv20.chromium.org"}}, {{"Sec-WebSocket-Protocol", "chatv11.chromium.org, chatv20.chromium.org"}}); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: " "'Sec-WebSocket-Protocol' header must not appear " "more than once in a response", failure_message()); } // Unmatched sub-protocol should be rejected. TEST_P(WebSocketMultiProtocolStreamCreateTest, UnmatchedSubProtocolInResponse) { AddSSLData(); std::vector sub_protocols; sub_protocols.push_back("chatv11.chromium.org"); sub_protocols.push_back("chatv20.chromium.org"); CreateAndConnectStandard( "wss://www.example.org/testing_path", sub_protocols, {}, {{"Sec-WebSocket-Protocol", "chatv11.chromium.org, chatv20.chromium.org"}}, {{"Sec-WebSocket-Protocol", "chatv21.chromium.org"}}); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Protocol' header value 'chatv21.chromium.org' " "in response does not match any of sent values", failure_message()); } // permessage-deflate extension basic success case. TEST_P(WebSocketStreamCreateExtensionTest, PerMessageDeflateSuccess) { CreateAndConnectWithExtensions("permessage-deflate"); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); } // permessage-deflate extensions success with all parameters. TEST_P(WebSocketStreamCreateExtensionTest, PerMessageDeflateParamsSuccess) { CreateAndConnectWithExtensions( "permessage-deflate; client_no_context_takeover; " "server_max_window_bits=11; client_max_window_bits=13; " "server_no_context_takeover"); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); } // Verify that incoming messages are actually decompressed with // permessage-deflate enabled. TEST_P(WebSocketStreamCreateExtensionTest, PerMessageDeflateInflates) { AddSSLData(); SetAdditionalResponseData(std::string( "\xc1\x07" // WebSocket header (FIN + RSV1, Text payload 7 bytes) "\xf2\x48\xcd\xc9\xc9\x07\x00", // "Hello" DEFLATE compressed 9)); CreateAndConnectStandard( "wss://www.example.org/testing_path", NoSubProtocols(), {}, {}, {{"Sec-WebSocket-Extensions", "permessage-deflate"}}); WaitUntilConnectDone(); ASSERT_TRUE(stream_); std::vector> frames; TestCompletionCallback callback; int rv = stream_->ReadFrames(&frames, callback.callback()); rv = callback.GetResult(rv); ASSERT_THAT(rv, IsOk()); ASSERT_EQ(1U, frames.size()); ASSERT_EQ(5U, frames[0]->header.payload_length); EXPECT_EQ(std::string("Hello"), std::string(frames[0]->payload, frames[0]->header.payload_length)); } // Unknown extension in the response is rejected TEST_P(WebSocketStreamCreateExtensionTest, UnknownExtension) { CreateAndConnectWithExtensions("x-unknown-extension"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Found an unsupported extension 'x-unknown-extension' " "in 'Sec-WebSocket-Extensions' header", failure_message()); } // Malformed extensions are rejected (this file does not cover all possible // parse failures, as the parser is covered thoroughly by its own unit tests). TEST_P(WebSocketStreamCreateExtensionTest, MalformedExtension) { CreateAndConnectWithExtensions(";"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: 'Sec-WebSocket-Extensions' header " "value is rejected by the parser: ;", failure_message()); } // The permessage-deflate extension may only be specified once. TEST_P(WebSocketStreamCreateExtensionTest, OnlyOnePerMessageDeflateAllowed) { base::HistogramTester histogram_tester; CreateAndConnectWithExtensions( "permessage-deflate, permessage-deflate; client_max_window_bits=10"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: " "Received duplicate permessage-deflate response", failure_message()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED_EXTENSIONS))); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult:: HTTP2_FAILED_EXTENSIONS))); } } // client_max_window_bits must have an argument TEST_P(WebSocketStreamCreateExtensionTest, NoMaxWindowBitsArgument) { CreateAndConnectWithExtensions("permessage-deflate; client_max_window_bits"); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ( "Error during WebSocket handshake: Error in permessage-deflate: " "client_max_window_bits must have value", failure_message()); } // Other cases for permessage-deflate parameters are tested in // websocket_deflate_parameters_test.cc. // TODO(ricea): Check that WebSocketDeflateStream is initialised with the // arguments from the server. This is difficult because the data written to the // socket is randomly masked. // Additional Sec-WebSocket-Accept headers should be rejected. TEST_P(WebSocketStreamCreateTest, DoubleAccept) { CreateAndConnectStandard( "ws://www.example.org/", NoSubProtocols(), {}, {}, {{"Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}}); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Accept' header must not appear " "more than once in a response", failure_message()); } // When upgrading an HTTP/1 connection, response code 200 is invalid and must be // rejected. Response code 101 means success. On the other hand, when // requesting a WebSocket stream over HTTP/2, response code 101 is invalid and // must be rejected. Response code 200 means success. TEST_P(WebSocketMultiProtocolStreamCreateTest, InvalidStatusCode) { base::HistogramTester histogram_tester; AddSSLData(); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { 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("wss://www.example.org/", NoSubProtocols(), {}, {}, kInvalidStatusCodeResponse); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); SetHttp2ResponseStatus("101"); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); } WaitUntilConnectDone(); stream_request_.reset(); EXPECT_TRUE(has_failed()); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 200", failure_message()); EXPECT_EQ(failure_response_code(), 200); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::INVALID_STATUS))); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 101", failure_message()); EXPECT_EQ(failure_response_code(), 101); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult:: HTTP2_INVALID_STATUS))); } } // Redirects are not followed (according to the WHATWG WebSocket API, which // overrides RFC6455 for browser applications). TEST_P(WebSocketMultiProtocolStreamCreateTest, RedirectsRejected) { AddSSLData(); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { 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: wss://www.example.org/other\r\n" "\r\n" "Moved

Moved

"; CreateAndConnectCustomResponse("wss://www.example.org/", NoSubProtocols(), {}, {}, kRedirectResponse); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); SetHttp2ResponseStatus("302"); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); } WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 302", failure_message()); } // Malformed responses should be rejected. HttpStreamParser will accept just // about any garbage in the middle of the headers. To make it give up, the junk // has to be at the start of the response. Even then, it just gets treated as an // HTTP/0.9 response. TEST_P(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://www.example.org/", NoSubProtocols(), {}, {}, kMalformedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: Invalid status line", failure_message()); } // Upgrade header must be present. TEST_P(WebSocketStreamCreateTest, MissingUpgradeHeader) { base::HistogramTester histogram_tester; 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://www.example.org/", NoSubProtocols(), {}, {}, kMissingUpgradeResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: 'Upgrade' header is missing", failure_message()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED_UPGRADE))); } // There must only be one upgrade header. TEST_P(WebSocketStreamCreateTest, DoubleUpgradeHeader) { CreateAndConnectStandard("ws://www.example.org/", NoSubProtocols(), {}, {}, {{"Upgrade", "HTTP/2.0"}}); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Upgrade' header must not appear more than once in a response", failure_message()); } // There must only be one correct upgrade header. TEST_P(WebSocketStreamCreateTest, IncorrectUpgradeHeader) { static const char kMissingUpgradeResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Upgrade: hogefuga\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kMissingUpgradeResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Upgrade' header value is not 'WebSocket': hogefuga", failure_message()); } // Connection header must be present. TEST_P(WebSocketStreamCreateTest, MissingConnectionHeader) { base::HistogramTester histogram_tester; 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://www.example.org/", NoSubProtocols(), {}, {}, kMissingConnectionResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Connection' header is missing", failure_message()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED_CONNECTION))); } // Connection header must contain "Upgrade". TEST_P(WebSocketStreamCreateTest, IncorrectConnectionHeader) { static const char kMissingConnectionResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Connection: hogefuga\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kMissingConnectionResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Connection' header value must contain 'Upgrade'", failure_message()); } // Connection header is permitted to contain other tokens. TEST_P(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://www.example.org/", NoSubProtocols(), {}, {}, kAdditionalConnectionTokenResponse); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // Sec-WebSocket-Accept header must be present. TEST_P(WebSocketStreamCreateTest, MissingSecWebSocketAccept) { base::HistogramTester histogram_tester; static const char kMissingAcceptResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "\r\n"; CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kMissingAcceptResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "'Sec-WebSocket-Accept' header is missing", failure_message()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED_ACCEPT))); } // Sec-WebSocket-Accept header must match the key that was sent. TEST_P(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://www.example.org/", NoSubProtocols(), {}, {}, kIncorrectAcceptResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error during WebSocket handshake: " "Incorrect 'Sec-WebSocket-Accept' header value", failure_message()); } // Cancellation works. TEST_P(WebSocketStreamCreateTest, Cancellation) { CreateAndConnectStandard("ws://www.example.org/", NoSubProtocols(), {}, {}, {}); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); } // Connect failure must look just like negotiation failure. TEST_P(WebSocketStreamCreateTest, ConnectionFailure) { std::unique_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_REFUSED", failure_message()); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); } // Connect timeout must look just like any other failure. TEST_P(WebSocketStreamCreateTest, ConnectionTimeout) { std::unique_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(ASYNC, ERR_CONNECTION_TIMED_OUT)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_TIMED_OUT", failure_message()); } // The server doesn't respond to the opening handshake. TEST_P(WebSocketStreamCreateTest, HandshakeTimeout) { std::unique_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); auto timer = std::make_unique(); base::WeakPtr weak_timer = timer->AsWeakPtr(); SetTimer(std::move(timer)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); EXPECT_FALSE(has_failed()); ASSERT_TRUE(weak_timer.get()); EXPECT_TRUE(weak_timer->IsRunning()); weak_timer->Fire(); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("WebSocket opening handshake timed out", failure_message()); ASSERT_TRUE(weak_timer.get()); EXPECT_FALSE(weak_timer->IsRunning()); } // When the connection establishes the timer should be stopped. TEST_P(WebSocketStreamCreateTest, HandshakeTimerOnSuccess) { auto timer = std::make_unique(); base::WeakPtr weak_timer = timer->AsWeakPtr(); SetTimer(std::move(timer)); CreateAndConnectStandard("ws://www.example.org/", NoSubProtocols(), {}, {}, {}); ASSERT_TRUE(weak_timer); EXPECT_TRUE(weak_timer->IsRunning()); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(weak_timer); EXPECT_FALSE(weak_timer->IsRunning()); } // When the connection fails the timer should be stopped. TEST_P(WebSocketStreamCreateTest, HandshakeTimerOnFailure) { std::unique_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data( MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED)); auto timer = std::make_unique(); base::WeakPtr weak_timer = timer->AsWeakPtr(); SetTimer(std::move(timer)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); ASSERT_TRUE(weak_timer.get()); EXPECT_TRUE(weak_timer->IsRunning()); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_REFUSED", failure_message()); ASSERT_TRUE(weak_timer.get()); EXPECT_FALSE(weak_timer->IsRunning()); } // Cancellation during connect works. TEST_P(WebSocketStreamCreateTest, CancellationDuringConnect) { std::unique_ptr socket_data(BuildNullSocketData()); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); } // Cancellation during write of the request headers works. TEST_P(WebSocketStreamCreateTest, CancellationDuringWrite) { // First write never completes. MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_IO_PENDING, 0)}; auto socket_data = std::make_unique(base::span(), writes); auto* socket_data_ptr = socket_data.get(); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data_ptr->AllWriteDataConsumed()); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_TRUE(request_info_); EXPECT_FALSE(response_info_); } // Cancellation during read of the response headers works. TEST_P(WebSocketStreamCreateTest, CancellationDuringRead) { std::string request = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); MockWrite writes[] = {MockWrite(ASYNC, 0, request.c_str())}; MockRead reads[] = { MockRead(SYNCHRONOUS, ERR_IO_PENDING, 1), }; std::unique_ptr socket_data( BuildSocketData(reads, writes)); SequencedSocketData* socket_data_raw_ptr = socket_data.get(); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data_raw_ptr->AllReadDataConsumed()); stream_request_.reset(); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); EXPECT_FALSE(stream_); EXPECT_TRUE(request_info_); EXPECT_FALSE(response_info_); } // Over-size response headers (> 256KB) should not cause a crash. This is a // regression test for crbug.com/339456. It is based on the layout test // "cookie-flood.html". TEST_P(WebSocketStreamCreateTest, VeryLargeResponseHeaders) { base::HistogramTester histogram_tester; std::string set_cookie_headers; set_cookie_headers.reserve(24 * 20000); for (int i = 0; i < 20000; ++i) { set_cookie_headers += base::StringPrintf("Set-Cookie: ws-%d=1\r\n", i); } ASSERT_GT(set_cookie_headers.size(), 256U * 1024U); CreateAndConnectStringResponse("ws://www.example.org/", NoSubProtocols(), set_cookie_headers); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_FALSE(response_info_); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::FAILED))); } // If the remote host closes the connection without sending headers, we should // log the console message "Connection closed before receiving a handshake // response". TEST_P(WebSocketStreamCreateTest, NoResponse) { base::HistogramTester histogram_tester; std::string request = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); MockWrite writes[] = {MockWrite(ASYNC, request.data(), request.size(), 0)}; MockRead reads[] = {MockRead(ASYNC, 0, 1)}; std::unique_ptr socket_data( BuildSocketData(reads, writes)); SequencedSocketData* socket_data_raw_ptr = socket_data.get(); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_TRUE(socket_data_raw_ptr->AllReadDataConsumed()); EXPECT_TRUE(has_failed()); EXPECT_FALSE(stream_); EXPECT_FALSE(response_info_); EXPECT_EQ("Connection closed before receiving a handshake response", failure_message()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::EMPTY_RESPONSE))); } TEST_P(WebSocketStreamCreateTest, SelfSignedCertificateFailure) { auto ssl_socket_data = std::make_unique( ASYNC, ERR_CERT_AUTHORITY_INVALID); ssl_socket_data->ssl_info.cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_socket_data->ssl_info.cert.get()); url_request_context_host_.AddSSLSocketDataProvider( std::move(ssl_socket_data)); std::unique_ptr raw_socket_data(BuildNullSocketData()); CreateAndConnectRawExpectations("wss://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(raw_socket_data)); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); ASSERT_TRUE(ssl_error_callbacks_); ssl_error_callbacks_->CancelSSLRequest(ERR_CERT_AUTHORITY_INVALID, &ssl_info_); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); } TEST_P(WebSocketStreamCreateTest, SelfSignedCertificateSuccess) { auto ssl_socket_data = std::make_unique( ASYNC, ERR_CERT_AUTHORITY_INVALID); ssl_socket_data->ssl_info.cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_socket_data->ssl_info.cert.get()); url_request_context_host_.AddSSLSocketDataProvider( std::move(ssl_socket_data)); url_request_context_host_.AddSSLSocketDataProvider( std::make_unique(ASYNC, OK)); AddRawExpectations(BuildNullSocketData()); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); // WaitUntilConnectDone doesn't work in this case. base::RunLoop().RunUntilIdle(); ASSERT_TRUE(ssl_error_callbacks_); ssl_error_callbacks_->ContinueSSLRequest(); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); } // If the server requests authorisation, but we have no credentials, the // connection should fail cleanly. TEST_P(WebSocketStreamCreateBasicAuthTest, FailureNoCredentials) { CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kUnauthorizedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("HTTP Authentication failed; no valid credentials available", failure_message()); EXPECT_FALSE(response_info_); } TEST_P(WebSocketStreamCreateBasicAuthTest, SuccessPasswordInUrl) { CreateAndConnectAuthHandshake("ws://foo:bar@www.example.org/", "Zm9vOmJhcg==", WebSocketStandardResponse(std::string())); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(response_info_); EXPECT_EQ(101, response_info_->headers->response_code()); } TEST_P(WebSocketStreamCreateBasicAuthTest, FailureIncorrectPasswordInUrl) { CreateAndConnectAuthHandshake("ws://foo:baz@www.example.org/", "Zm9vOmJheg==", kUnauthorizedResponse); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_FALSE(response_info_); } TEST_P(WebSocketStreamCreateBasicAuthTest, SuccessfulConnectionReuse) { std::string request1 = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); std::string response1 = kUnauthorizedResponse; std::string request2 = WebSocketStandardRequest( "/", "www.example.org", Origin(), {{"Authorization", "Basic Zm9vOmJhcg=="}}, /*extra_headers=*/{}); std::string response2 = WebSocketStandardResponse(std::string()); MockWrite writes[] = { MockWrite(SYNCHRONOUS, 0, request1.c_str()), MockWrite(SYNCHRONOUS, 2, request2.c_str()), }; MockRead reads[3] = { MockRead(SYNCHRONOUS, 1, response1.c_str()), MockRead(SYNCHRONOUS, 3, response2.c_str()), MockRead(SYNCHRONOUS, ERR_IO_PENDING, 4), }; CreateAndConnectRawExpectations("ws://foo:bar@www.example.org/", NoSubProtocols(), HttpRequestHeaders(), BuildSocketData(reads, writes)); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(response_info_); EXPECT_EQ(101, response_info_->headers->response_code()); } TEST_P(WebSocketStreamCreateBasicAuthTest, OnAuthRequiredCancelAuth) { CreateAndConnectCustomResponse("ws://www.example.org/", NoSubProtocols(), {}, {}, kUnauthorizedResponse); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); on_auth_required_rv_ = ERR_IO_PENDING; WaitUntilOnAuthRequired(); EXPECT_FALSE(stream_); EXPECT_FALSE(has_failed()); std::move(on_auth_required_callback_).Run(nullptr); WaitUntilConnectDone(); EXPECT_FALSE(stream_); EXPECT_TRUE(has_failed()); } TEST_P(WebSocketStreamCreateBasicAuthTest, OnAuthRequiredSetAuth) { CreateAndConnectRawExpectations( "ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), helper_.BuildAuthSocketData(kUnauthorizedResponse, RequestExpectation("Zm9vOmJheg=="), WebSocketStandardResponse(std::string()))); EXPECT_FALSE(request_info_); EXPECT_FALSE(response_info_); on_auth_required_rv_ = ERR_IO_PENDING; WaitUntilOnAuthRequired(); EXPECT_FALSE(stream_); EXPECT_FALSE(has_failed()); AuthCredentials credentials(u"foo", u"baz"); std::move(on_auth_required_callback_).Run(&credentials); WaitUntilConnectDone(); EXPECT_TRUE(stream_); EXPECT_FALSE(has_failed()); } // Digest auth has the same connection semantics as Basic auth, so we can // generally assume that whatever works for Basic auth will also work for // Digest. There's just one test here, to confirm that it works at all. TEST_P(WebSocketStreamCreateDigestAuthTest, DigestPasswordInUrl) { CreateAndConnectRawExpectations( "ws://FooBar:pass@www.example.org/", NoSubProtocols(), HttpRequestHeaders(), helper_.BuildAuthSocketData(kUnauthorizedResponse, kAuthorizedRequest, WebSocketStandardResponse(std::string()))); WaitUntilConnectDone(); EXPECT_FALSE(has_failed()); EXPECT_TRUE(stream_); ASSERT_TRUE(response_info_); EXPECT_EQ(101, response_info_->headers->response_code()); } TEST_P(WebSocketMultiProtocolStreamCreateTest, Incomplete) { base::HistogramTester histogram_tester; AddSSLData(); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { std::string request = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); MockRead reads[] = {MockRead(ASYNC, ERR_IO_PENDING, 0)}; MockWrite writes[] = {MockWrite(ASYNC, 1, request.c_str())}; CreateAndConnectRawExpectations("wss://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), BuildSocketData(reads, writes)); base::RunLoop().RunUntilIdle(); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::INCOMPLETE))); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::HTTP2_INCOMPLETE))); } } TEST_P(WebSocketMultiProtocolStreamCreateTest, Http2StreamReset) { AddSSLData(); if (stream_type_ == BASIC_HANDSHAKE_STREAM) { // This is a dummy transaction to avoid crash in ~URLRequestContext(). CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); } else { DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM); base::HistogramTester histogram_tester; SetResetWebSocketHttp2Stream(true); CreateAndConnectStandard("wss://www.example.org/", NoSubProtocols(), {}, {}, {}); base::RunLoop().RunUntilIdle(); stream_request_.reset(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Stream closed with error: net::ERR_HTTP2_PROTOCOL_ERROR", failure_message()); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ( 1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult::HTTP2_FAILED))); } } TEST_P(WebSocketStreamCreateTest, HandleErrConnectionClosed) { base::HistogramTester histogram_tester; static const char kTruncatedResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Cache-Control: no-sto"; std::string request = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); MockRead reads[] = { MockRead(SYNCHRONOUS, 1, kTruncatedResponse), MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED, 2), }; MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, request.c_str())}; std::unique_ptr socket_data( BuildSocketData(reads, writes)); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); stream_request_.reset(); auto samples = histogram_tester.GetHistogramSamplesSinceCreation( "Net.WebSocket.HandshakeResult2"); EXPECT_EQ(1, samples->TotalCount()); EXPECT_EQ(1, samples->GetCount(static_cast( WebSocketHandshakeStreamBase::HandshakeResult:: FAILED_SWITCHING_PROTOCOLS))); } TEST_P(WebSocketStreamCreateTest, HandleErrTunnelConnectionFailed) { static const char kConnectRequest[] = "CONNECT www.example.org:80 HTTP/1.1\r\n" "Host: www.example.org:80\r\n" "Proxy-Connection: keep-alive\r\n" "\r\n"; static const char kProxyResponse[] = "HTTP/1.1 403 Forbidden\r\n" "Content-Type: text/html\r\n" "Content-Length: 9\r\n" "Connection: keep-alive\r\n" "\r\n" "Forbidden"; MockRead reads[] = {MockRead(SYNCHRONOUS, 1, kProxyResponse)}; MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, kConnectRequest)}; std::unique_ptr socket_data( BuildSocketData(reads, writes)); url_request_context_host_.SetProxyConfig("https=proxy:8000"); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); WaitUntilConnectDone(); EXPECT_TRUE(has_failed()); EXPECT_EQ("Establishing a tunnel via proxy server failed.", failure_message()); } TEST_P(WebSocketStreamCreateTest, CancelSSLRequestAfterDelete) { auto ssl_socket_data = std::make_unique( ASYNC, ERR_CERT_AUTHORITY_INVALID); ssl_socket_data->ssl_info.cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_socket_data->ssl_info.cert.get()); url_request_context_host_.AddSSLSocketDataProvider( std::move(ssl_socket_data)); MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_CONNECTION_RESET, 0)}; MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET, 1)}; std::unique_ptr raw_socket_data( BuildSocketData(reads, writes)); CreateAndConnectRawExpectations("wss://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(raw_socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); ASSERT_TRUE(ssl_error_callbacks_); stream_request_.reset(); ssl_error_callbacks_->CancelSSLRequest(ERR_CERT_AUTHORITY_INVALID, &ssl_info_); } TEST_P(WebSocketStreamCreateTest, ContinueSSLRequestAfterDelete) { auto ssl_socket_data = std::make_unique( ASYNC, ERR_CERT_AUTHORITY_INVALID); ssl_socket_data->ssl_info.cert = ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der"); ASSERT_TRUE(ssl_socket_data->ssl_info.cert.get()); url_request_context_host_.AddSSLSocketDataProvider( std::move(ssl_socket_data)); MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_CONNECTION_RESET, 0)}; MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET, 1)}; std::unique_ptr raw_socket_data( BuildSocketData(reads, writes)); CreateAndConnectRawExpectations("wss://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(raw_socket_data)); base::RunLoop().RunUntilIdle(); EXPECT_FALSE(has_failed()); ASSERT_TRUE(ssl_error_callbacks_); stream_request_.reset(); ssl_error_callbacks_->ContinueSSLRequest(); } TEST_P(WebSocketStreamCreateTest, HandleConnectionCloseInFirstSegment) { std::string request = WebSocketStandardRequest( "/", "www.example.org", Origin(), /*send_additional_request_headers=*/{}, /*extra_headers=*/{}); // The response headers are immediately followed by a close frame, length 11, // code 1013, reason "Try Again". std::string close_body = "\x03\xf5Try Again"; std::string response = WebSocketStandardResponse(std::string()) + "\x88" + static_cast(close_body.size()) + close_body; MockRead reads[] = { MockRead(SYNCHRONOUS, response.data(), response.size(), 1), MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED, 2), }; MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, request.c_str())}; std::unique_ptr socket_data( BuildSocketData(reads, writes)); socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK)); CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(), HttpRequestHeaders(), std::move(socket_data)); WaitUntilConnectDone(); ASSERT_TRUE(stream_); std::vector> frames; TestCompletionCallback callback1; int rv1 = stream_->ReadFrames(&frames, callback1.callback()); rv1 = callback1.GetResult(rv1); ASSERT_THAT(rv1, IsOk()); ASSERT_EQ(1U, frames.size()); EXPECT_EQ(frames[0]->header.opcode, WebSocketFrameHeader::kOpCodeClose); EXPECT_TRUE(frames[0]->header.final); EXPECT_EQ(close_body, std::string(frames[0]->payload, frames[0]->header.payload_length)); std::vector> empty_frames; TestCompletionCallback callback2; int rv2 = stream_->ReadFrames(&empty_frames, callback2.callback()); rv2 = callback2.GetResult(rv2); ASSERT_THAT(rv2, IsError(ERR_CONNECTION_CLOSED)); } } // namespace } // namespace net