1810 lines
71 KiB
C++
1810 lines
71 KiB
C++
// Copyright 2013 The Chromium Authors
|
|
// 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/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<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::MockOneShotTimer,
|
|
public base::SupportsWeakPtr<MockWeakTimer> {
|
|
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<HandshakeStreamType>,
|
|
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<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::OneShotTimer> 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(),
|
|
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<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;
|
|
write_settings[spdy::SETTINGS_MAX_HEADER_LIST_SIZE] =
|
|
kSpdyMaxHeaderListSize;
|
|
write_settings[spdy::SETTINGS_ENABLE_PUSH] = 0;
|
|
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<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_.emplace_back(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.
|
|
URLRequestContext* 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, /*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<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(),
|
|
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<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(),
|
|
/*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<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(),
|
|
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<base::OneShotTimer> 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<spdy::SpdySerializedFrame> frames_;
|
|
|
|
// Store MockRead and MockWrite objects that have pointers to above data.
|
|
std::vector<MockRead> reads_;
|
|
std::vector<MockWrite> 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<SequencedSocketData> 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<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(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<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(failure_response_code(), 200);
|
|
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(failure_response_code(), 101);
|
|
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>();
|
|
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>();
|
|
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>();
|
|
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)};
|
|
auto socket_data =
|
|
std::make_unique<SequencedSocketData>(base::span<MockRead>(), 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<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(), /*send_additional_request_headers=*/{},
|
|
/*extra_headers=*/{});
|
|
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_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<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 ~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<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(), /*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<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());
|
|
}
|
|
|
|
TEST_P(WebSocketStreamCreateTest, CancelSSLRequestAfterDelete) {
|
|
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));
|
|
|
|
MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_CONNECTION_RESET, 0)};
|
|
MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET, 1)};
|
|
std::unique_ptr<SequencedSocketData> 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<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));
|
|
|
|
MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_CONNECTION_RESET, 0)};
|
|
MockWrite writes[] = {MockWrite(SYNCHRONOUS, ERR_CONNECTION_RESET, 1)};
|
|
std::unique_ptr<SequencedSocketData> 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<char>(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<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();
|
|
ASSERT_TRUE(stream_);
|
|
|
|
std::vector<std::unique_ptr<WebSocketFrame>> 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<std::unique_ptr<WebSocketFrame>> 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
|