blob: b6c8fbf96ca78120b13d3bc9a45cb08b336ec4b3 [file] [log] [blame]
// 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 <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/macros.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/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/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/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<SequencedSocketData> BuildSocketData(
base::span<MockRead> reads,
base::span<MockWrite> writes) {
auto socket_data = std::make_unique<SequencedSocketData>(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<SequencedSocketData> BuildNullSocketData() {
return std::make_unique<SequencedSocketData>();
}
class MockWeakTimer : public base::MockTimer,
public base::SupportsWeakPtr<MockWeakTimer> {
public:
MockWeakTimer(bool retain_user_task, bool is_repeating)
: MockTimer(retain_user_task, is_repeating) {}
};
const char kOrigin[] = "http://www.example.org";
static url::Origin Origin() {
return url::Origin::Create(GURL(kOrigin));
}
static GURL SiteForCookies() {
return GURL("http://www.example.org/foobar");
}
class WebSocketStreamCreateTest : public TestWithParam<HandshakeStreamType>,
public WebSocketStreamCreateTestBase {
protected:
WebSocketStreamCreateTest()
: stream_type_(GetParam()),
http2_response_status_("200"),
reset_websocket_http2_stream_(false),
sequence_number_(0) {}
~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<SequencedSocketData> socket_data) {
url_request_context_host_.AddRawExpectations(std::move(socket_data));
}
void AddSSLData() {
auto ssl_data = std::make_unique<SSLSocketDataProvider>(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<base::Timer> 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<std::string>& 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(),
WebSocketExtraHeadersToString(send_additional_request_headers),
WebSocketExtraHeadersToString(extra_request_headers)),
WebSocketStandardResponse(
WebSocketExtraHeadersToString(extra_response_headers)) +
additional_data_);
CreateAndConnectStream(socket_url, sub_protocols, Origin(),
SiteForCookies(),
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_.push_back(spdy::SpdySerializedFrame(
const_cast<char*>(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;
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, arraysize(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::SpdyHeaderBlock 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<std::string> extra_response_header_keys;
std::vector<const char*> 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_.push_back(MockRead(ASYNC, 0, sequence_number_++));
auto socket_data = std::make_unique<SequencedSocketData>(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.
TestURLRequestContext* context =
url_request_context_host_.GetURLRequestContext();
TestDelegate delegate;
std::unique_ptr<URLRequest> request = context->CreateRequest(
GURL("https://www.example.org/"), DEFAULT_PRIORITY, &delegate,
TRAFFIC_ANNOTATION_FOR_TESTS);
request->Start();
EXPECT_TRUE(request->is_pending());
delegate.RunUntilComplete();
EXPECT_FALSE(request->is_pending());
CreateAndConnectStream(socket_url, sub_protocols, Origin(),
SiteForCookies(),
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<std::string>& 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(),
WebSocketExtraHeadersToString(send_additional_request_headers),
WebSocketExtraHeadersToString(extra_request_headers)),
response_body);
CreateAndConnectStream(socket_url, sub_protocols, Origin(),
SiteForCookies(),
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<std::string>& 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(), "", ""),
WebSocketStandardResponse(extra_response_headers));
CreateAndConnectStream(socket_url, sub_protocols, Origin(),
SiteForCookies(), HttpRequestHeaders(), nullptr);
}
// Like CreateAndConnectStandard(), but take raw mock data.
void CreateAndConnectRawExpectations(
base::StringPiece url,
const std::vector<std::string>& sub_protocols,
const HttpRequestHeaders& additional_headers,
std::unique_ptr<SequencedSocketData> socket_data) {
ASSERT_EQ(BASIC_HANDSHAKE_STREAM, stream_type_);
AddRawExpectations(std::move(socket_data));
CreateAndConnectStream(GURL(url), sub_protocols, Origin(), SiteForCookies(),
additional_headers, std::move(timer_));
}
private:
void AddWrite(const spdy::SpdySerializedFrame* frame) {
writes_.push_back(
MockWrite(ASYNC, frame->data(), frame->size(), sequence_number_++));
}
void AddRead(const spdy::SpdySerializedFrame* frame) {
reads_.push_back(
MockRead(ASYNC, frame->data(), frame->size(), sequence_number_++));
}
protected:
const HandshakeStreamType stream_type_;
private:
std::unique_ptr<base::Timer> timer_;
std::string additional_data_;
const char* http2_response_status_;
bool reset_websocket_http2_stream_;
SpdyTestUtil spdy_util_;
NetLogWithSource log_;
int sequence_number_;
// Store mock HTTP/2 data.
std::vector<spdy::SpdySerializedFrame> frames_;
// Store MockRead and MockWrite objects that have pointers to above data.
std::vector<MockRead> reads_;
std::vector<MockWrite> writes_;
};
INSTANTIATE_TEST_CASE_P(,
WebSocketStreamCreateTest,
Values(BASIC_HANDSHAKE_STREAM));
using WebSocketMultiProtocolStreamCreateTest = WebSocketStreamCreateTest;
INSTANTIATE_TEST_CASE_P(,
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_CASE_P(,
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() : reads1_(), writes1_(), reads2_(), writes2_() {}
std::unique_ptr<SequencedSocketData> BuildSocketData1(
const std::string& response) {
request1_ =
WebSocketStandardRequest("/", "www.example.org", Origin(), "", "");
writes1_[0] = MockWrite(SYNCHRONOUS, 0, request1_.c_str());
response1_ = response;
reads1_[0] = MockRead(SYNCHRONOUS, 1, response1_.c_str());
reads1_[1] = MockRead(SYNCHRONOUS, OK, 2); // Close connection
return BuildSocketData(reads1_, writes1_);
}
std::unique_ptr<SequencedSocketData> BuildSocketData2(
const std::string& request,
const std::string& response) {
request2_ = request;
response2_ = response;
writes2_[0] = MockWrite(SYNCHRONOUS, 0, request2_.c_str());
reads2_[0] = MockRead(SYNCHRONOUS, 1, response2_.c_str());
return BuildSocketData(reads2_, writes2_);
}
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 reads1_[2];
MockWrite writes1_[1];
MockRead reads2_[1];
MockWrite writes2_[1];
DISALLOW_COPY_AND_ASSIGN(CommonAuthTestHelper);
};
// Data and methods for BasicAuth tests.
class WebSocketStreamCreateBasicAuthTest : public WebSocketStreamCreateTest {
protected:
void CreateAndConnectAuthHandshake(base::StringPiece url,
base::StringPiece base64_user_pass,
base::StringPiece response2) {
AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse));
CreateAndConnectRawExpectations(
url, NoSubProtocols(), HttpRequestHeaders(),
helper_.BuildSocketData2(RequestExpectation(base64_user_pass),
response2.as_string()));
}
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_CASE_P(,
WebSocketStreamCreateBasicAuthTest,
Values(BASIC_HANDSHAKE_STREAM));
class WebSocketStreamCreateDigestAuthTest : public WebSocketStreamCreateTest {
protected:
static const char kUnauthorizedResponse[];
static const char kAuthorizedRequest[];
CommonAuthTestHelper helper_;
};
INSTANTIATE_TEST_CASE_P(,
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<int>(
WebSocketHandshakeStreamBase::HandshakeResult::CONNECTED)));
} else {
DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM);
EXPECT_EQ(
1,
samples->GetCount(static_cast<int>(
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<HeaderKeyValuePair> 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<HeaderKeyValuePair> 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<HeaderKeyValuePair> 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<std::string> 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<int>(
WebSocketHandshakeStreamBase::HandshakeResult::FAILED_SUBPROTO)));
} else {
DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM);
EXPECT_EQ(1, samples->GetCount(static_cast<int>(
WebSocketHandshakeStreamBase::HandshakeResult::
HTTP2_FAILED_SUBPROTO)));
}
}
// Missing sub-protocol response is rejected.
TEST_P(WebSocketMultiProtocolStreamCreateTest, UnacceptedSubProtocol) {
AddSSLData();
std::vector<std::string> 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<std::string> 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<std::string> 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<std::unique_ptr<WebSocketFrame>> 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("Hello", std::string(frames[0]->data->data(), 5));
}
// 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<int>(
WebSocketHandshakeStreamBase::HandshakeResult::FAILED_EXTENSIONS)));
} else {
DCHECK_EQ(stream_type_, HTTP2_HANDSHAKE_STREAM);
EXPECT_EQ(1, samples->GetCount(static_cast<int>(
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(
1, samples->GetCount(static_cast<int>(
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(1, samples->GetCount(static_cast<int>(
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"
"<title>Moved</title><h1>Moved</h1>";
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<int>(
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<int>(
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<int>(
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<SequencedSocketData> 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<SequencedSocketData> 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<SequencedSocketData> socket_data(BuildNullSocketData());
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING));
auto timer = std::make_unique<MockWeakTimer>(false, false);
base::WeakPtr<MockWeakTimer> 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<MockWeakTimer>(false, false);
base::WeakPtr<MockWeakTimer> 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<SequencedSocketData> socket_data(BuildNullSocketData());
socket_data->set_connect_data(
MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED));
auto timer = std::make_unique<MockWeakTimer>(false, false);
base::WeakPtr<MockWeakTimer> 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<SequencedSocketData> 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)};
SequencedSocketData* socket_data(
new SequencedSocketData(base::span<MockRead>(), writes));
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK));
CreateAndConnectRawExpectations("ws://www.example.org/", NoSubProtocols(),
HttpRequestHeaders(),
base::WrapUnique(socket_data));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(socket_data->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(), "", "");
MockWrite writes[] = {MockWrite(ASYNC, 0, request.c_str())};
MockRead reads[] = {
MockRead(SYNCHRONOUS, ERR_IO_PENDING, 1),
};
std::unique_ptr<SequencedSocketData> 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<int>(
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(), "", "");
MockWrite writes[] = {MockWrite(ASYNC, request.data(), request.size(), 0)};
MockRead reads[] = {MockRead(ASYNC, 0, 1)};
std::unique_ptr<SequencedSocketData> 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<int>(
WebSocketHandshakeStreamBase::HandshakeResult::EMPTY_RESPONSE)));
}
TEST_P(WebSocketStreamCreateTest, SelfSignedCertificateFailure) {
auto ssl_socket_data = std::make_unique<SSLSocketDataProvider>(
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<SequencedSocketData> 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<SSLSocketDataProvider>(
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<SSLSocketDataProvider>(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_TRUE(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_TRUE(response_info_);
}
TEST_P(WebSocketStreamCreateBasicAuthTest, SuccessfulConnectionReuse) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
WebSocketBasicHandshakeStream ::kWebSocketHandshakeReuseConnection);
std::string request1 =
"GET / HTTP/1.1\r\n"
"Host: www.example.org\r\n"
"Connection: Keep-Alive, Upgrade\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\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";
std::string response1 = kUnauthorizedResponse;
std::string request2 =
"GET / HTTP/1.1\r\n"
"Host: www.example.org\r\n"
"Connection: Keep-Alive, Upgrade\r\n"
"Pragma: no-cache\r\n"
"Cache-Control: no-cache\r\n"
"Authorization: Basic Zm9vOmJhcg==\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";
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) {
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());
AuthCredentials credentials(base::ASCIIToUTF16("foo"),
base::ASCIIToUTF16("baz"));
std::move(on_auth_required_callback_).Run(&credentials);
// As we are re-establishing the connection with additional credentials,
// add new expectations.
AddRawExpectations(
helper_.BuildSocketData2(RequestExpectation("Zm9vOmJheg=="),
WebSocketStandardResponse(std::string())));
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) {
AddRawExpectations(helper_.BuildSocketData1(kUnauthorizedResponse));
CreateAndConnectRawExpectations(
"ws://FooBar:pass@www.example.org/", NoSubProtocols(),
HttpRequestHeaders(),
helper_.BuildSocketData2(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(), "", "");
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<int>(
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<int>(
WebSocketHandshakeStreamBase::HandshakeResult::HTTP2_INCOMPLETE)));
}
}
TEST_P(WebSocketMultiProtocolStreamCreateTest, Http2StreamReset) {
AddSSLData();
if (stream_type_ == BASIC_HANDSHAKE_STREAM) {
// This is a dummy transaction to avoid crash in ~TestURLRequestContext().
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_SPDY_PROTOCOL_ERROR",
failure_message());
auto samples = histogram_tester.GetHistogramSamplesSinceCreation(
"Net.WebSocket.HandshakeResult2");
EXPECT_EQ(1, samples->TotalCount());
EXPECT_EQ(
1, samples->GetCount(static_cast<int>(
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(), "", "");
MockRead reads[] = {
MockRead(SYNCHRONOUS, 1, kTruncatedResponse),
MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED, 2),
};
MockWrite writes[] = {MockWrite(SYNCHRONOUS, 0, request.c_str())};
std::unique_ptr<SequencedSocketData> 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<int>(
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<SequencedSocketData> 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());
}
} // namespace
} // namespace net