blob: b7229f93188a43543b4871d98f5f418de70503ff [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/memory/scoped_vector.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 "net/base/net_errors.h"
#include "net/base/test_data_directory.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/socket/client_socket_handle.h"
#include "net/socket/socket_test_util.h"
#include "net/test/cert_test_util.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_handshake_request_info.h"
#include "net/websockets/websocket_handshake_response_info.h"
#include "net/websockets/websocket_handshake_stream_create_helper.h"
#include "net/websockets/websocket_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace net {
namespace {
typedef std::pair<std::string, std::string> HeaderKeyValuePair;
std::vector<HeaderKeyValuePair> ToVector(const HttpRequestHeaders& headers) {
HttpRequestHeaders::Iterator it(headers);
std::vector<HeaderKeyValuePair> result;
while (it.GetNext())
result.push_back(HeaderKeyValuePair(it.name(), it.value()));
return result;
}
std::vector<HeaderKeyValuePair> ToVector(const HttpResponseHeaders& headers) {
void* iter = NULL;
std::string name, value;
std::vector<HeaderKeyValuePair> result;
while (headers.EnumerateHeaderLines(&iter, &name, &value))
result.push_back(HeaderKeyValuePair(name, value));
return result;
}
// A sub-class of WebSocketHandshakeStreamCreateHelper which always sets a
// deterministic key to use in the WebSocket handshake.
class DeterministicKeyWebSocketHandshakeStreamCreateHelper
: public WebSocketHandshakeStreamCreateHelper {
public:
DeterministicKeyWebSocketHandshakeStreamCreateHelper(
WebSocketStream::ConnectDelegate* connect_delegate,
const std::vector<std::string>& requested_subprotocols)
: WebSocketHandshakeStreamCreateHelper(connect_delegate,
requested_subprotocols) {}
virtual void OnStreamCreated(WebSocketBasicHandshakeStream* stream) OVERRIDE {
stream->SetWebSocketKeyForTesting("dGhlIHNhbXBsZSBub25jZQ==");
}
};
class WebSocketStreamCreateTest : public ::testing::Test {
public:
WebSocketStreamCreateTest() : has_failed_(false), ssl_fatal_(false) {}
void CreateAndConnectCustomResponse(
const std::string& socket_url,
const std::string& socket_path,
const std::vector<std::string>& sub_protocols,
const std::string& origin,
const std::string& extra_request_headers,
const std::string& response_body) {
url_request_context_host_.SetExpectations(
WebSocketStandardRequest(socket_path, origin, extra_request_headers),
response_body);
CreateAndConnectStream(socket_url, sub_protocols, origin);
}
// |extra_request_headers| and |extra_response_headers| must end in "\r\n" or
// errors like "Unable to perform synchronous IO while stopped" will occur.
void CreateAndConnectStandard(const std::string& socket_url,
const std::string& socket_path,
const std::vector<std::string>& sub_protocols,
const std::string& origin,
const std::string& extra_request_headers,
const std::string& extra_response_headers) {
CreateAndConnectCustomResponse(
socket_url,
socket_path,
sub_protocols,
origin,
extra_request_headers,
WebSocketStandardResponse(extra_response_headers));
}
void CreateAndConnectRawExpectations(
const std::string& socket_url,
const std::vector<std::string>& sub_protocols,
const std::string& origin,
scoped_ptr<DeterministicSocketData> socket_data) {
url_request_context_host_.AddRawExpectations(socket_data.Pass());
CreateAndConnectStream(socket_url, sub_protocols, origin);
}
// A wrapper for CreateAndConnectStreamForTesting that knows about our default
// parameters.
void CreateAndConnectStream(const std::string& socket_url,
const std::vector<std::string>& sub_protocols,
const std::string& origin) {
for (size_t i = 0; i < ssl_data_.size(); ++i) {
scoped_ptr<SSLSocketDataProvider> ssl_data(ssl_data_[i]);
ssl_data_[i] = NULL;
url_request_context_host_.AddSSLSocketDataProvider(ssl_data.Pass());
}
ssl_data_.clear();
scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate(
new TestConnectDelegate(this));
WebSocketStream::ConnectDelegate* delegate = connect_delegate.get();
scoped_ptr<WebSocketHandshakeStreamCreateHelper> create_helper(
new DeterministicKeyWebSocketHandshakeStreamCreateHelper(
delegate, sub_protocols));
stream_request_ = ::net::CreateAndConnectStreamForTesting(
GURL(socket_url),
create_helper.Pass(),
url::Origin(origin),
url_request_context_host_.GetURLRequestContext(),
BoundNetLog(),
connect_delegate.Pass());
}
static void RunUntilIdle() { base::RunLoop().RunUntilIdle(); }
// A simple function to make the tests more readable. Creates an empty vector.
static std::vector<std::string> NoSubProtocols() {
return std::vector<std::string>();
}
const std::string& failure_message() const { return failure_message_; }
bool has_failed() const { return has_failed_; }
class TestConnectDelegate : public WebSocketStream::ConnectDelegate {
public:
explicit TestConnectDelegate(WebSocketStreamCreateTest* owner)
: owner_(owner) {}
virtual void OnSuccess(scoped_ptr<WebSocketStream> stream) OVERRIDE {
stream.swap(owner_->stream_);
}
virtual void OnFailure(const std::string& message) OVERRIDE {
owner_->has_failed_ = true;
owner_->failure_message_ = message;
}
virtual void OnStartOpeningHandshake(
scoped_ptr<WebSocketHandshakeRequestInfo> request) OVERRIDE {
if (owner_->request_info_)
ADD_FAILURE();
owner_->request_info_ = request.Pass();
}
virtual void OnFinishOpeningHandshake(
scoped_ptr<WebSocketHandshakeResponseInfo> response) OVERRIDE {
if (owner_->response_info_)
ADD_FAILURE();
owner_->response_info_ = response.Pass();
}
virtual void OnSSLCertificateError(
scoped_ptr<WebSocketEventInterface::SSLErrorCallbacks>
ssl_error_callbacks,
const SSLInfo& ssl_info,
bool fatal) OVERRIDE {
owner_->ssl_error_callbacks_ = ssl_error_callbacks.Pass();
owner_->ssl_info_ = ssl_info;
owner_->ssl_fatal_ = fatal;
}
private:
WebSocketStreamCreateTest* owner_;
};
WebSocketTestURLRequestContextHost url_request_context_host_;
scoped_ptr<WebSocketStreamRequest> stream_request_;
// Only set if the connection succeeded.
scoped_ptr<WebSocketStream> stream_;
// Only set if the connection failed.
std::string failure_message_;
bool has_failed_;
scoped_ptr<WebSocketHandshakeRequestInfo> request_info_;
scoped_ptr<WebSocketHandshakeResponseInfo> response_info_;
scoped_ptr<WebSocketEventInterface::SSLErrorCallbacks> ssl_error_callbacks_;
SSLInfo ssl_info_;
bool ssl_fatal_;
ScopedVector<SSLSocketDataProvider> ssl_data_;
};
// There are enough tests of the Sec-WebSocket-Extensions header that they
// deserve their own test fixture.
class WebSocketStreamCreateExtensionTest : public WebSocketStreamCreateTest {
public:
// 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) {
CreateAndConnectStandard(
"ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://localhost",
"",
"Sec-WebSocket-Extensions: " + extensions_header_value + "\r\n");
RunUntilIdle();
}
};
class WebSocketStreamCreateUMATest : public ::testing::Test {
public:
// This enum should match with the enum in Delegate in websocket_stream.cc.
enum HandshakeResult {
INCOMPLETE,
CONNECTED,
FAILED,
NUM_HANDSHAKE_RESULT_TYPES,
};
class StreamCreation : public WebSocketStreamCreateTest {
virtual void TestBody() OVERRIDE {}
};
scoped_ptr<base::HistogramSamples> GetSamples(const std::string& name) {
base::HistogramBase* histogram =
base::StatisticsRecorder::FindHistogram(name);
return histogram ? histogram->SnapshotSamples()
: scoped_ptr<base::HistogramSamples>();
}
};
// Confirm that the basic case works as expected.
TEST_F(WebSocketStreamCreateTest, SimpleSuccess) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost", "", "");
EXPECT_FALSE(request_info_);
EXPECT_FALSE(response_info_);
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_TRUE(stream_);
EXPECT_TRUE(request_info_);
EXPECT_TRUE(response_info_);
}
TEST_F(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://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kResponse);
EXPECT_FALSE(request_info_);
EXPECT_FALSE(response_info_);
RunUntilIdle();
EXPECT_TRUE(stream_);
ASSERT_TRUE(request_info_);
ASSERT_TRUE(response_info_);
std::vector<HeaderKeyValuePair> request_headers =
ToVector(request_info_->headers);
// We examine the contents of request_info_ and response_info_
// mainly only in this test case.
EXPECT_EQ(GURL("ws://localhost/"), request_info_->url);
EXPECT_EQ(GURL("ws://localhost/"), response_info_->url);
EXPECT_EQ(101, response_info_->status_code);
EXPECT_EQ("Switching Protocols", response_info_->status_text);
ASSERT_EQ(12u, request_headers.size());
EXPECT_EQ(HeaderKeyValuePair("Host", "localhost"), 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://localhost"),
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 =
ToVector(*response_info_->headers);
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]);
}
// Confirm that the stream isn't established until the message loop runs.
TEST_F(WebSocketStreamCreateTest, NeedsToRunLoop) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost", "", "");
EXPECT_FALSE(has_failed());
EXPECT_FALSE(stream_);
}
// Check the path is used.
TEST_F(WebSocketStreamCreateTest, PathIsUsed) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://localhost",
"",
"");
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_TRUE(stream_);
}
// Check that the origin is used.
TEST_F(WebSocketStreamCreateTest, OriginIsUsed) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://google.com",
"",
"");
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_TRUE(stream_);
}
// Check that sub-protocols are sent and parsed.
TEST_F(WebSocketStreamCreateTest, SubProtocolIsUsed) {
std::vector<std::string> sub_protocols;
sub_protocols.push_back("chatv11.chromium.org");
sub_protocols.push_back("chatv20.chromium.org");
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
sub_protocols,
"http://google.com",
"Sec-WebSocket-Protocol: chatv11.chromium.org, "
"chatv20.chromium.org\r\n",
"Sec-WebSocket-Protocol: chatv20.chromium.org\r\n");
RunUntilIdle();
EXPECT_TRUE(stream_);
EXPECT_FALSE(has_failed());
EXPECT_EQ("chatv20.chromium.org", stream_->GetSubProtocol());
}
// Unsolicited sub-protocols are rejected.
TEST_F(WebSocketStreamCreateTest, UnsolicitedSubProtocol) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://google.com",
"",
"Sec-WebSocket-Protocol: chatv20.chromium.org\r\n");
RunUntilIdle();
EXPECT_FALSE(stream_);
EXPECT_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());
}
// Missing sub-protocol response is rejected.
TEST_F(WebSocketStreamCreateTest, UnacceptedSubProtocol) {
std::vector<std::string> sub_protocols;
sub_protocols.push_back("chat.example.com");
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
sub_protocols,
"http://localhost",
"Sec-WebSocket-Protocol: chat.example.com\r\n",
"");
RunUntilIdle();
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_F(WebSocketStreamCreateTest, MultipleSubProtocolsInResponse) {
std::vector<std::string> sub_protocols;
sub_protocols.push_back("chatv11.chromium.org");
sub_protocols.push_back("chatv20.chromium.org");
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
sub_protocols,
"http://google.com",
"Sec-WebSocket-Protocol: chatv11.chromium.org, "
"chatv20.chromium.org\r\n",
"Sec-WebSocket-Protocol: chatv11.chromium.org, "
"chatv20.chromium.org\r\n");
RunUntilIdle();
EXPECT_FALSE(stream_);
EXPECT_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_F(WebSocketStreamCreateTest, UnmatchedSubProtocolInResponse) {
std::vector<std::string> sub_protocols;
sub_protocols.push_back("chatv11.chromium.org");
sub_protocols.push_back("chatv20.chromium.org");
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
sub_protocols,
"http://google.com",
"Sec-WebSocket-Protocol: chatv11.chromium.org, "
"chatv20.chromium.org\r\n",
"Sec-WebSocket-Protocol: chatv21.chromium.org\r\n");
RunUntilIdle();
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_F(WebSocketStreamCreateExtensionTest, PerMessageDeflateSuccess) {
CreateAndConnectWithExtensions("permessage-deflate");
EXPECT_TRUE(stream_);
EXPECT_FALSE(has_failed());
}
// permessage-deflate extensions success with all parameters.
TEST_F(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_F(WebSocketStreamCreateExtensionTest, PerMessageDeflateInflates) {
CreateAndConnectCustomResponse(
"ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://localhost",
"",
WebSocketStandardResponse(
"Sec-WebSocket-Extensions: permessage-deflate\r\n") +
std::string(
"\xc1\x07" // WebSocket header (FIN + RSV1, Text payload 7 bytes)
"\xf2\x48\xcd\xc9\xc9\x07\x00", // "Hello" DEFLATE compressed
9));
RunUntilIdle();
ASSERT_TRUE(stream_);
ScopedVector<WebSocketFrame> frames;
CompletionCallback callback;
ASSERT_EQ(OK, stream_->ReadFrames(&frames, callback));
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_F(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_F(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_F(WebSocketStreamCreateExtensionTest, OnlyOnePerMessageDeflateAllowed) {
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());
}
// permessage-deflate parameters may not be duplicated.
TEST_F(WebSocketStreamCreateExtensionTest, NoDuplicateParameters) {
CreateAndConnectWithExtensions(
"permessage-deflate; client_no_context_takeover; "
"client_no_context_takeover");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received duplicate permessage-deflate extension parameter "
"client_no_context_takeover",
failure_message());
}
// permessage-deflate parameters must start with "client_" or "server_"
TEST_F(WebSocketStreamCreateExtensionTest, BadParameterPrefix) {
CreateAndConnectWithExtensions(
"permessage-deflate; absurd_no_context_takeover");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received an unexpected permessage-deflate extension parameter",
failure_message());
}
// permessage-deflate parameters must be either *_no_context_takeover or
// *_max_window_bits
TEST_F(WebSocketStreamCreateExtensionTest, BadParameterSuffix) {
CreateAndConnectWithExtensions(
"permessage-deflate; client_max_content_bits=5");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received an unexpected permessage-deflate extension parameter",
failure_message());
}
// *_no_context_takeover parameters must not have an argument
TEST_F(WebSocketStreamCreateExtensionTest, BadParameterValue) {
CreateAndConnectWithExtensions(
"permessage-deflate; client_no_context_takeover=true");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid client_no_context_takeover parameter",
failure_message());
}
// *_max_window_bits must have an argument
TEST_F(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());
}
// *_max_window_bits must be an integer
TEST_F(WebSocketStreamCreateExtensionTest, MaxWindowBitsValueInteger) {
CreateAndConnectWithExtensions(
"permessage-deflate; server_max_window_bits=banana");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid server_max_window_bits parameter",
failure_message());
}
// *_max_window_bits must be >= 8
TEST_F(WebSocketStreamCreateExtensionTest, MaxWindowBitsValueTooSmall) {
CreateAndConnectWithExtensions(
"permessage-deflate; server_max_window_bits=7");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid server_max_window_bits parameter",
failure_message());
}
// *_max_window_bits must be <= 15
TEST_F(WebSocketStreamCreateExtensionTest, MaxWindowBitsValueTooBig) {
CreateAndConnectWithExtensions(
"permessage-deflate; client_max_window_bits=16");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid client_max_window_bits parameter",
failure_message());
}
// *_max_window_bits must not start with 0
TEST_F(WebSocketStreamCreateExtensionTest, MaxWindowBitsValueStartsWithZero) {
CreateAndConnectWithExtensions(
"permessage-deflate; client_max_window_bits=08");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid client_max_window_bits parameter",
failure_message());
}
// *_max_window_bits must not start with +
TEST_F(WebSocketStreamCreateExtensionTest, MaxWindowBitsValueStartsWithPlus) {
CreateAndConnectWithExtensions(
"permessage-deflate; server_max_window_bits=+9");
EXPECT_FALSE(stream_);
EXPECT_TRUE(has_failed());
EXPECT_EQ(
"Error during WebSocket handshake: Error in permessage-deflate: "
"Received invalid server_max_window_bits parameter",
failure_message());
}
// 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_F(WebSocketStreamCreateTest, DoubleAccept) {
CreateAndConnectStandard(
"ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n");
RunUntilIdle();
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());
}
// Response code 200 must be rejected.
TEST_F(WebSocketStreamCreateTest, InvalidStatusCode) {
static const char kInvalidStatusCodeResponse[] =
"HTTP/1.1 200 OK\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kInvalidStatusCodeResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: Unexpected response code: 200",
failure_message());
}
// Redirects are not followed (according to the WHATWG WebSocket API, which
// overrides RFC6455 for browser applications).
TEST_F(WebSocketStreamCreateTest, RedirectsRejected) {
static const char kRedirectResponse[] =
"HTTP/1.1 302 Moved Temporarily\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 34\r\n"
"Connection: keep-alive\r\n"
"Location: ws://localhost/other\r\n"
"\r\n"
"<title>Moved</title><h1>Moved</h1>";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kRedirectResponse);
RunUntilIdle();
EXPECT_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_F(WebSocketStreamCreateTest, MalformedResponse) {
static const char kMalformedResponse[] =
"220 mx.google.com ESMTP\r\n"
"HTTP/1.1 101 OK\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMalformedResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: Invalid status line",
failure_message());
}
// Upgrade header must be present.
TEST_F(WebSocketStreamCreateTest, MissingUpgradeHeader) {
static const char kMissingUpgradeResponse[] =
"HTTP/1.1 101 Switching Protocols\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMissingUpgradeResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: 'Upgrade' header is missing",
failure_message());
}
// There must only be one upgrade header.
TEST_F(WebSocketStreamCreateTest, DoubleUpgradeHeader) {
CreateAndConnectStandard(
"ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"", "Upgrade: HTTP/2.0\r\n");
RunUntilIdle();
EXPECT_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_F(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://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMissingUpgradeResponse);
RunUntilIdle();
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_F(WebSocketStreamCreateTest, MissingConnectionHeader) {
static const char kMissingConnectionResponse[] =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMissingConnectionResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: "
"'Connection' header is missing",
failure_message());
}
// Connection header must contain "Upgrade".
TEST_F(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://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMissingConnectionResponse);
RunUntilIdle();
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_F(WebSocketStreamCreateTest, AdditionalTokenInConnectionHeader) {
static const char kAdditionalConnectionTokenResponse[] =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade, Keep-Alive\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kAdditionalConnectionTokenResponse);
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_TRUE(stream_);
}
// Sec-WebSocket-Accept header must be present.
TEST_F(WebSocketStreamCreateTest, MissingSecWebSocketAccept) {
static const char kMissingAcceptResponse[] =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kMissingAcceptResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: "
"'Sec-WebSocket-Accept' header is missing",
failure_message());
}
// Sec-WebSocket-Accept header must match the key that was sent.
TEST_F(WebSocketStreamCreateTest, WrongSecWebSocketAccept) {
static const char kIncorrectAcceptResponse[] =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: x/byyPZ2tOFvJCGkkugcKvqhhPk=\r\n"
"\r\n";
CreateAndConnectCustomResponse("ws://localhost/",
"/",
NoSubProtocols(),
"http://localhost",
"",
kIncorrectAcceptResponse);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error during WebSocket handshake: "
"Incorrect 'Sec-WebSocket-Accept' header value",
failure_message());
}
// Cancellation works.
TEST_F(WebSocketStreamCreateTest, Cancellation) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost", "", "");
stream_request_.reset();
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_F(WebSocketStreamCreateTest, ConnectionFailure) {
scoped_ptr<DeterministicSocketData> socket_data(
new DeterministicSocketData(NULL, 0, NULL, 0));
socket_data->set_connect_data(
MockConnect(SYNCHRONOUS, ERR_CONNECTION_REFUSED));
CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(),
"http://localhost", socket_data.Pass());
RunUntilIdle();
EXPECT_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_F(WebSocketStreamCreateTest, ConnectionTimeout) {
scoped_ptr<DeterministicSocketData> socket_data(
new DeterministicSocketData(NULL, 0, NULL, 0));
socket_data->set_connect_data(
MockConnect(ASYNC, ERR_CONNECTION_TIMED_OUT));
CreateAndConnectRawExpectations("ws://localhost/", NoSubProtocols(),
"http://localhost", socket_data.Pass());
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_EQ("Error in connection establishment: net::ERR_CONNECTION_TIMED_OUT",
failure_message());
}
// Cancellation during connect works.
TEST_F(WebSocketStreamCreateTest, CancellationDuringConnect) {
scoped_ptr<DeterministicSocketData> socket_data(
new DeterministicSocketData(NULL, 0, NULL, 0));
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, ERR_IO_PENDING));
CreateAndConnectRawExpectations("ws://localhost/",
NoSubProtocols(),
"http://localhost",
socket_data.Pass());
stream_request_.reset();
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_FALSE(stream_);
}
// Cancellation during write of the request headers works.
TEST_F(WebSocketStreamCreateTest, CancellationDuringWrite) {
// We seem to need at least two operations in order to use SetStop().
MockWrite writes[] = {MockWrite(ASYNC, 0, "GET / HTTP/"),
MockWrite(ASYNC, 1, "1.1\r\n")};
// We keep a copy of the pointer so that we can call RunFor() on it later.
DeterministicSocketData* socket_data(
new DeterministicSocketData(NULL, 0, writes, arraysize(writes)));
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK));
socket_data->SetStop(1);
CreateAndConnectRawExpectations("ws://localhost/",
NoSubProtocols(),
"http://localhost",
make_scoped_ptr(socket_data));
socket_data->Run();
stream_request_.reset();
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_FALSE(stream_);
EXPECT_TRUE(request_info_);
EXPECT_FALSE(response_info_);
}
// Cancellation during read of the response headers works.
TEST_F(WebSocketStreamCreateTest, CancellationDuringRead) {
std::string request = WebSocketStandardRequest("/", "http://localhost", "");
MockWrite writes[] = {MockWrite(ASYNC, 0, request.c_str())};
MockRead reads[] = {
MockRead(ASYNC, 1, "HTTP/1.1 101 Switching Protocols\r\nUpgr"),
};
DeterministicSocketData* socket_data(new DeterministicSocketData(
reads, arraysize(reads), writes, arraysize(writes)));
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK));
socket_data->SetStop(1);
CreateAndConnectRawExpectations("ws://localhost/",
NoSubProtocols(),
"http://localhost",
make_scoped_ptr(socket_data));
socket_data->Run();
stream_request_.reset();
RunUntilIdle();
EXPECT_FALSE(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_F(WebSocketStreamCreateTest, VeryLargeResponseHeaders) {
std::string set_cookie_headers;
set_cookie_headers.reserve(45 * 10000);
for (int i = 0; i < 10000; ++i) {
set_cookie_headers +=
base::StringPrintf("Set-Cookie: WK-websocket-test-flood-%d=1\r\n", i);
}
CreateAndConnectStandard("ws://localhost/", "/", NoSubProtocols(),
"http://localhost", "", set_cookie_headers);
RunUntilIdle();
EXPECT_TRUE(has_failed());
EXPECT_FALSE(response_info_);
}
// If the remote host closes the connection without sending headers, we should
// log the console message "Connection closed before receiving a handshake
// response".
TEST_F(WebSocketStreamCreateTest, NoResponse) {
std::string request = WebSocketStandardRequest("/", "http://localhost", "");
MockWrite writes[] = {MockWrite(ASYNC, request.data(), request.size(), 0)};
MockRead reads[] = {MockRead(ASYNC, 0, 1)};
DeterministicSocketData* socket_data(new DeterministicSocketData(
reads, arraysize(reads), writes, arraysize(writes)));
socket_data->set_connect_data(MockConnect(SYNCHRONOUS, OK));
CreateAndConnectRawExpectations("ws://localhost/",
NoSubProtocols(),
"http://localhost",
make_scoped_ptr(socket_data));
socket_data->RunFor(2);
EXPECT_TRUE(has_failed());
EXPECT_FALSE(stream_);
EXPECT_FALSE(response_info_);
EXPECT_EQ("Connection closed before receiving a handshake response",
failure_message());
}
TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateFailure) {
ssl_data_.push_back(
new SSLSocketDataProvider(ASYNC, ERR_CERT_AUTHORITY_INVALID));
ssl_data_[0]->cert =
ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der");
ASSERT_TRUE(ssl_data_[0]->cert);
scoped_ptr<DeterministicSocketData> raw_socket_data(
new DeterministicSocketData(NULL, 0, NULL, 0));
CreateAndConnectRawExpectations("wss://localhost/",
NoSubProtocols(),
"http://localhost",
raw_socket_data.Pass());
RunUntilIdle();
EXPECT_FALSE(has_failed());
ASSERT_TRUE(ssl_error_callbacks_);
ssl_error_callbacks_->CancelSSLRequest(ERR_CERT_AUTHORITY_INVALID,
&ssl_info_);
RunUntilIdle();
EXPECT_TRUE(has_failed());
}
TEST_F(WebSocketStreamCreateTest, SelfSignedCertificateSuccess) {
scoped_ptr<SSLSocketDataProvider> ssl_data(
new SSLSocketDataProvider(ASYNC, ERR_CERT_AUTHORITY_INVALID));
ssl_data->cert =
ImportCertFromFile(GetTestCertsDirectory(), "unittest.selfsigned.der");
ASSERT_TRUE(ssl_data->cert);
ssl_data_.push_back(ssl_data.release());
ssl_data.reset(new SSLSocketDataProvider(ASYNC, OK));
ssl_data_.push_back(ssl_data.release());
url_request_context_host_.AddRawExpectations(
make_scoped_ptr(new DeterministicSocketData(NULL, 0, NULL, 0)));
CreateAndConnectStandard(
"wss://localhost/", "/", NoSubProtocols(), "http://localhost", "", "");
RunUntilIdle();
ASSERT_TRUE(ssl_error_callbacks_);
ssl_error_callbacks_->ContinueSSLRequest();
RunUntilIdle();
EXPECT_FALSE(has_failed());
EXPECT_TRUE(stream_);
}
TEST_F(WebSocketStreamCreateUMATest, Incomplete) {
const std::string name("Net.WebSocket.HandshakeResult");
scoped_ptr<base::HistogramSamples> original(GetSamples(name));
{
StreamCreation creation;
creation.CreateAndConnectStandard("ws://localhost/",
"/",
creation.NoSubProtocols(),
"http://localhost",
"",
"");
}
scoped_ptr<base::HistogramSamples> samples(GetSamples(name));
ASSERT_TRUE(samples);
if (original) {
samples->Subtract(*original); // Cancel the original values.
}
EXPECT_EQ(1, samples->GetCount(INCOMPLETE));
EXPECT_EQ(0, samples->GetCount(CONNECTED));
EXPECT_EQ(0, samples->GetCount(FAILED));
}
TEST_F(WebSocketStreamCreateUMATest, Connected) {
const std::string name("Net.WebSocket.HandshakeResult");
scoped_ptr<base::HistogramSamples> original(GetSamples(name));
{
StreamCreation creation;
creation.CreateAndConnectStandard("ws://localhost/",
"/",
creation.NoSubProtocols(),
"http://localhost",
"",
"");
creation.RunUntilIdle();
}
scoped_ptr<base::HistogramSamples> samples(GetSamples(name));
ASSERT_TRUE(samples);
if (original) {
samples->Subtract(*original); // Cancel the original values.
}
EXPECT_EQ(0, samples->GetCount(INCOMPLETE));
EXPECT_EQ(1, samples->GetCount(CONNECTED));
EXPECT_EQ(0, samples->GetCount(FAILED));
}
TEST_F(WebSocketStreamCreateUMATest, Failed) {
const std::string name("Net.WebSocket.HandshakeResult");
scoped_ptr<base::HistogramSamples> original(GetSamples(name));
{
StreamCreation creation;
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";
creation.CreateAndConnectCustomResponse("ws://localhost/",
"/",
creation.NoSubProtocols(),
"http://localhost",
"",
kInvalidStatusCodeResponse);
creation.RunUntilIdle();
}
scoped_ptr<base::HistogramSamples> samples(GetSamples(name));
ASSERT_TRUE(samples);
if (original) {
samples->Subtract(*original); // Cancel the original values.
}
EXPECT_EQ(1, samples->GetCount(INCOMPLETE));
EXPECT_EQ(0, samples->GetCount(CONNECTED));
EXPECT_EQ(0, samples->GetCount(FAILED));
}
} // namespace
} // namespace net