// 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 <string>
#include <vector>
#include "base/run_loop.h"
#include "net/base/net_errors.h"
#include "net/socket/client_socket_handle.h"
#include "net/socket/socket_test_util.h"
#include "net/url_request/url_request_test_util.h"
#include "net/websockets/websocket_basic_handshake_stream.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"
namespace net {
namespace {
// A sub-class of WebSocketHandshakeStreamCreateHelper which always sets a
// deterministic key to use in the WebSocket handshake.
class DeterministicKeyWebSocketHandshakeStreamCreateHelper
: public WebSocketHandshakeStreamCreateHelper {
public:
DeterministicKeyWebSocketHandshakeStreamCreateHelper(
const std::vector<std::string>& requested_subprotocols)
: WebSocketHandshakeStreamCreateHelper(requested_subprotocols) {}
virtual WebSocketHandshakeStreamBase* CreateBasicStream(
scoped_ptr<ClientSocketHandle> connection,
bool using_proxy) OVERRIDE {
WebSocketHandshakeStreamCreateHelper::CreateBasicStream(connection.Pass(),
using_proxy);
// This will break in an obvious way if the type created by
// CreateBasicStream() changes.
static_cast<WebSocketBasicHandshakeStream*>(stream())
->SetWebSocketKeyForTesting("dGhlIHNhbXBsZSBub25jZQ==");
return stream();
}
};
class WebSocketStreamCreateTest : public ::testing::Test {
protected:
WebSocketStreamCreateTest() : websocket_error_(0) {}
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_.SetRawExpectations(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) {
stream_request_ = ::net::CreateAndConnectStreamForTesting(
GURL(socket_url),
scoped_ptr<WebSocketHandshakeStreamCreateHelper>(
new DeterministicKeyWebSocketHandshakeStreamCreateHelper(
sub_protocols)),
GURL(origin),
url_request_context_host_.GetURLRequestContext(),
BoundNetLog(),
scoped_ptr<WebSocketStream::ConnectDelegate>(
new TestConnectDelegate(this)));
}
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>();
}
uint16 error() const { return websocket_error_; }
class TestConnectDelegate : public WebSocketStream::ConnectDelegate {
public:
TestConnectDelegate(WebSocketStreamCreateTest* owner) : owner_(owner) {}
virtual void OnSuccess(scoped_ptr<WebSocketStream> stream) OVERRIDE {
stream.swap(owner_->stream_);
}
virtual void OnFailure(uint16 websocket_error) OVERRIDE {
owner_->websocket_error_ = websocket_error;
}
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. 0 otherwise.
uint16 websocket_error_;
};
// Confirm that the basic case works as expected.
TEST_F(WebSocketStreamCreateTest, SimpleSuccess) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", "");
RunUntilIdle();
EXPECT_TRUE(stream_);
}
// Confirm that the stream isn't established until the message loop runs.
TEST_F(WebSocketStreamCreateTest, NeedsToRunLoop) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", "");
EXPECT_FALSE(stream_);
}
// Check the path is used.
TEST_F(WebSocketStreamCreateTest, PathIsUsed) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://localhost/",
"",
"");
RunUntilIdle();
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_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_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_EQ(1006, error());
}
// Missing sub-protocol response is rejected.
TEST_F(WebSocketStreamCreateTest, UnacceptedSubProtocol) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
std::vector<std::string>(1, "chat.example.com"),
"http://localhost/",
"Sec-WebSocket-Protocol: chat.example.com\r\n",
"");
RunUntilIdle();
EXPECT_FALSE(stream_);
EXPECT_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// Unknown extension in the response is rejected
TEST_F(WebSocketStreamCreateTest, UnknownExtension) {
CreateAndConnectStandard("ws://localhost/testing_path",
"/testing_path",
NoSubProtocols(),
"http://localhost/",
"",
"Sec-WebSocket-Extensions: x-unknown-extension\r\n");
RunUntilIdle();
EXPECT_FALSE(stream_);
EXPECT_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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_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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// Cancellation works.
TEST_F(WebSocketStreamCreateTest, Cancellation) {
CreateAndConnectStandard(
"ws://localhost/", "/", NoSubProtocols(), "http://localhost/", "", "");
stream_request_.reset();
RunUntilIdle();
EXPECT_FALSE(stream_);
}
// 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_EQ(1006, error());
}
// 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_EQ(1006, error());
}
// 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(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(stream_);
}
// 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(stream_);
}
} // namespace
} // namespace net