// 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_job.h" #include <string> #include <vector> #include "base/bind.h" #include "base/bind_helpers.h" #include "base/callback.h" #include "base/memory/ref_counted.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "net/base/completion_callback.h" #include "net/base/net_errors.h" #include "net/base/test_completion_callback.h" #include "net/cookies/cookie_store.h" #include "net/cookies/cookie_store_test_helpers.h" #include "net/dns/mock_host_resolver.h" #include "net/http/http_transaction_factory.h" #include "net/http/transport_security_state.h" #include "net/proxy/proxy_service.h" #include "net/socket/next_proto.h" #include "net/socket/socket_test_util.h" #include "net/socket_stream/socket_stream.h" #include "net/spdy/spdy_session.h" #include "net/spdy/spdy_websocket_test_util.h" #include "net/ssl/ssl_config_service.h" #include "net/url_request/url_request_context.h" #include "net/websockets/websocket_throttle.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" #include "url/gurl.h" namespace net { namespace { class MockSocketStream : public SocketStream { public: MockSocketStream(const GURL& url, SocketStream::Delegate* delegate, URLRequestContext* context, CookieStore* cookie_store) : SocketStream(url, delegate, context, cookie_store) {} virtual void Connect() OVERRIDE {} virtual bool SendData(const char* data, int len) OVERRIDE { sent_data_ += std::string(data, len); return true; } virtual void Close() OVERRIDE {} virtual void RestartWithAuth( const AuthCredentials& credentials) OVERRIDE { } virtual void DetachDelegate() OVERRIDE { delegate_ = NULL; } const std::string& sent_data() const { return sent_data_; } protected: virtual ~MockSocketStream() {} private: std::string sent_data_; }; class MockSocketStreamDelegate : public SocketStream::Delegate { public: MockSocketStreamDelegate() : amount_sent_(0), allow_all_cookies_(true) {} void set_allow_all_cookies(bool allow_all_cookies) { allow_all_cookies_ = allow_all_cookies; } virtual ~MockSocketStreamDelegate() {} void SetOnStartOpenConnection(const base::Closure& callback) { on_start_open_connection_ = callback; } void SetOnConnected(const base::Closure& callback) { on_connected_ = callback; } void SetOnSentData(const base::Closure& callback) { on_sent_data_ = callback; } void SetOnReceivedData(const base::Closure& callback) { on_received_data_ = callback; } void SetOnClose(const base::Closure& callback) { on_close_ = callback; } virtual int OnStartOpenConnection( SocketStream* socket, const CompletionCallback& callback) OVERRIDE { if (!on_start_open_connection_.is_null()) on_start_open_connection_.Run(); return OK; } virtual void OnConnected(SocketStream* socket, int max_pending_send_allowed) OVERRIDE { if (!on_connected_.is_null()) on_connected_.Run(); } virtual void OnSentData(SocketStream* socket, int amount_sent) OVERRIDE { amount_sent_ += amount_sent; if (!on_sent_data_.is_null()) on_sent_data_.Run(); } virtual void OnReceivedData(SocketStream* socket, const char* data, int len) OVERRIDE { received_data_ += std::string(data, len); if (!on_received_data_.is_null()) on_received_data_.Run(); } virtual void OnClose(SocketStream* socket) OVERRIDE { if (!on_close_.is_null()) on_close_.Run(); } virtual bool CanGetCookies(SocketStream* socket, const GURL& url) OVERRIDE { return allow_all_cookies_; } virtual bool CanSetCookie(SocketStream* request, const GURL& url, const std::string& cookie_line, CookieOptions* options) OVERRIDE { return allow_all_cookies_; } size_t amount_sent() const { return amount_sent_; } const std::string& received_data() const { return received_data_; } private: int amount_sent_; bool allow_all_cookies_; std::string received_data_; base::Closure on_start_open_connection_; base::Closure on_connected_; base::Closure on_sent_data_; base::Closure on_received_data_; base::Closure on_close_; }; class MockCookieStore : public CookieStore { public: struct Entry { GURL url; std::string cookie_line; CookieOptions options; }; MockCookieStore() {} bool SetCookieWithOptions(const GURL& url, const std::string& cookie_line, const CookieOptions& options) { Entry entry; entry.url = url; entry.cookie_line = cookie_line; entry.options = options; entries_.push_back(entry); return true; } std::string GetCookiesWithOptions(const GURL& url, const CookieOptions& options) { std::string result; for (size_t i = 0; i < entries_.size(); i++) { Entry& entry = entries_[i]; if (url == entry.url) { if (!result.empty()) { result += "; "; } result += entry.cookie_line; } } return result; } // CookieStore: virtual void SetCookieWithOptionsAsync( const GURL& url, const std::string& cookie_line, const CookieOptions& options, const SetCookiesCallback& callback) OVERRIDE { bool result = SetCookieWithOptions(url, cookie_line, options); if (!callback.is_null()) callback.Run(result); } virtual void GetCookiesWithOptionsAsync( const GURL& url, const CookieOptions& options, const GetCookiesCallback& callback) OVERRIDE { if (!callback.is_null()) callback.Run(GetCookiesWithOptions(url, options)); } virtual void GetAllCookiesForURLAsync( const GURL& url, const GetCookieListCallback& callback) OVERRIDE { ADD_FAILURE(); } virtual void DeleteCookieAsync(const GURL& url, const std::string& cookie_name, const base::Closure& callback) OVERRIDE { ADD_FAILURE(); } virtual void DeleteAllCreatedBetweenAsync( const base::Time& delete_begin, const base::Time& delete_end, const DeleteCallback& callback) OVERRIDE { ADD_FAILURE(); } virtual void DeleteAllCreatedBetweenForHostAsync( const base::Time delete_begin, const base::Time delete_end, const GURL& url, const DeleteCallback& callback) OVERRIDE { ADD_FAILURE(); } virtual void DeleteSessionCookiesAsync(const DeleteCallback&) OVERRIDE { ADD_FAILURE(); } virtual CookieMonster* GetCookieMonster() OVERRIDE { return NULL; } const std::vector<Entry>& entries() const { return entries_; } private: friend class base::RefCountedThreadSafe<MockCookieStore>; virtual ~MockCookieStore() {} std::vector<Entry> entries_; }; class MockSSLConfigService : public SSLConfigService { public: virtual void GetSSLConfig(SSLConfig* config) OVERRIDE {} protected: virtual ~MockSSLConfigService() {} }; class MockURLRequestContext : public URLRequestContext { public: explicit MockURLRequestContext(CookieStore* cookie_store) : transport_security_state_() { set_cookie_store(cookie_store); set_transport_security_state(&transport_security_state_); base::Time expiry = base::Time::Now() + base::TimeDelta::FromDays(1000); bool include_subdomains = false; transport_security_state_.AddHSTS("upgrademe.com", expiry, include_subdomains); } virtual ~MockURLRequestContext() {} private: TransportSecurityState transport_security_state_; }; class MockHttpTransactionFactory : public HttpTransactionFactory { public: MockHttpTransactionFactory(NextProto next_proto, OrderedSocketData* data, bool enable_websocket_over_spdy) { data_ = data; MockConnect connect_data(SYNCHRONOUS, OK); data_->set_connect_data(connect_data); session_deps_.reset(new SpdySessionDependencies(next_proto)); session_deps_->enable_websocket_over_spdy = enable_websocket_over_spdy; session_deps_->socket_factory->AddSocketDataProvider(data_); http_session_ = SpdySessionDependencies::SpdyCreateSession(session_deps_.get()); host_port_pair_.set_host("example.com"); host_port_pair_.set_port(80); spdy_session_key_ = SpdySessionKey(host_port_pair_, ProxyServer::Direct(), PRIVACY_MODE_DISABLED); session_ = CreateInsecureSpdySession( http_session_, spdy_session_key_, BoundNetLog()); } virtual int CreateTransaction( RequestPriority priority, scoped_ptr<HttpTransaction>* trans) OVERRIDE { NOTREACHED(); return ERR_UNEXPECTED; } virtual HttpCache* GetCache() OVERRIDE { NOTREACHED(); return NULL; } virtual HttpNetworkSession* GetSession() OVERRIDE { return http_session_.get(); } private: OrderedSocketData* data_; scoped_ptr<SpdySessionDependencies> session_deps_; scoped_refptr<HttpNetworkSession> http_session_; base::WeakPtr<SpdySession> session_; HostPortPair host_port_pair_; SpdySessionKey spdy_session_key_; }; class DeletingSocketStreamDelegate : public SocketStream::Delegate { public: DeletingSocketStreamDelegate() : delete_next_(false) {} // Since this class needs to be able to delete |job_|, it must be the only // reference holder (except for temporary references). Provide access to the // pointer for tests to use. WebSocketJob* job() { return job_.get(); } void set_job(WebSocketJob* job) { job_ = job; } // After calling this, the next call to a method on this delegate will delete // the WebSocketJob object. void set_delete_next(bool delete_next) { delete_next_ = delete_next; } void DeleteJobMaybe() { if (delete_next_) { job_->DetachContext(); job_->DetachDelegate(); job_ = NULL; } } // SocketStream::Delegate implementation // OnStartOpenConnection() is not implemented by SocketStreamDispatcherHost virtual void OnConnected(SocketStream* socket, int max_pending_send_allowed) OVERRIDE { DeleteJobMaybe(); } virtual void OnSentData(SocketStream* socket, int amount_sent) OVERRIDE { DeleteJobMaybe(); } virtual void OnReceivedData(SocketStream* socket, const char* data, int len) OVERRIDE { DeleteJobMaybe(); } virtual void OnClose(SocketStream* socket) OVERRIDE { DeleteJobMaybe(); } virtual void OnAuthRequired(SocketStream* socket, AuthChallengeInfo* auth_info) OVERRIDE { DeleteJobMaybe(); } virtual void OnSSLCertificateError(SocketStream* socket, const SSLInfo& ssl_info, bool fatal) OVERRIDE { DeleteJobMaybe(); } virtual void OnError(const SocketStream* socket, int error) OVERRIDE { DeleteJobMaybe(); } // CanGetCookies() and CanSetCookies() do not appear to be able to delete the // WebSocketJob object. private: scoped_refptr<WebSocketJob> job_; bool delete_next_; }; } // namespace class WebSocketJobTest : public PlatformTest, public ::testing::WithParamInterface<NextProto> { public: WebSocketJobTest() : spdy_util_(GetParam()), enable_websocket_over_spdy_(false) {} virtual void SetUp() OVERRIDE { stream_type_ = STREAM_INVALID; cookie_store_ = new MockCookieStore; context_.reset(new MockURLRequestContext(cookie_store_.get())); } virtual void TearDown() OVERRIDE { cookie_store_ = NULL; context_.reset(); websocket_ = NULL; socket_ = NULL; } void DoSendRequest() { EXPECT_TRUE(websocket_->SendData(kHandshakeRequestWithoutCookie, kHandshakeRequestWithoutCookieLength)); } void DoSendData() { if (received_data().size() == kHandshakeResponseWithoutCookieLength) websocket_->SendData(kDataHello, kDataHelloLength); } void DoSync() { sync_test_callback_.callback().Run(OK); } int WaitForResult() { return sync_test_callback_.WaitForResult(); } protected: enum StreamType { STREAM_INVALID, STREAM_MOCK_SOCKET, STREAM_SOCKET, STREAM_SPDY_WEBSOCKET, }; enum ThrottlingOption { THROTTLING_OFF, THROTTLING_ON, }; enum SpdyOption { SPDY_OFF, SPDY_ON, }; void InitWebSocketJob(const GURL& url, MockSocketStreamDelegate* delegate, StreamType stream_type) { DCHECK_NE(STREAM_INVALID, stream_type); stream_type_ = stream_type; websocket_ = new WebSocketJob(delegate); if (stream_type == STREAM_MOCK_SOCKET) socket_ = new MockSocketStream(url, websocket_.get(), context_.get(), NULL); if (stream_type == STREAM_SOCKET || stream_type == STREAM_SPDY_WEBSOCKET) { if (stream_type == STREAM_SPDY_WEBSOCKET) { http_factory_.reset(new MockHttpTransactionFactory( GetParam(), data_.get(), enable_websocket_over_spdy_)); context_->set_http_transaction_factory(http_factory_.get()); } ssl_config_service_ = new MockSSLConfigService(); context_->set_ssl_config_service(ssl_config_service_.get()); proxy_service_.reset(ProxyService::CreateDirect()); context_->set_proxy_service(proxy_service_.get()); host_resolver_.reset(new MockHostResolver); context_->set_host_resolver(host_resolver_.get()); socket_ = new SocketStream(url, websocket_.get(), context_.get(), NULL); socket_factory_.reset(new MockClientSocketFactory); DCHECK(data_.get()); socket_factory_->AddSocketDataProvider(data_.get()); socket_->SetClientSocketFactory(socket_factory_.get()); } websocket_->InitSocketStream(socket_.get()); // MockHostResolver resolves all hosts to 127.0.0.1; however, when we create // a WebSocketJob purely to block another one in a throttling test, we don't // perform a real connect. In that case, the following address is used // instead. IPAddressNumber ip; ParseIPLiteralToNumber("127.0.0.1", &ip); websocket_->addresses_ = AddressList::CreateFromIPAddress(ip, 80); } void SkipToConnecting() { websocket_->state_ = WebSocketJob::CONNECTING; ASSERT_TRUE(WebSocketThrottle::GetInstance()->PutInQueue(websocket_.get())); } WebSocketJob::State GetWebSocketJobState() { return websocket_->state_; } void CloseWebSocketJob() { if (websocket_->socket_.get()) { websocket_->socket_->DetachDelegate(); WebSocketThrottle::GetInstance()->RemoveFromQueue(websocket_.get()); } websocket_->state_ = WebSocketJob::CLOSED; websocket_->delegate_ = NULL; websocket_->socket_ = NULL; } SocketStream* GetSocket(SocketStreamJob* job) { return job->socket_.get(); } const std::string& sent_data() const { DCHECK_EQ(STREAM_MOCK_SOCKET, stream_type_); MockSocketStream* socket = static_cast<MockSocketStream*>(socket_.get()); DCHECK(socket); return socket->sent_data(); } const std::string& received_data() const { DCHECK_NE(STREAM_INVALID, stream_type_); MockSocketStreamDelegate* delegate = static_cast<MockSocketStreamDelegate*>(websocket_->delegate_); DCHECK(delegate); return delegate->received_data(); } void TestSimpleHandshake(); void TestSlowHandshake(); void TestHandshakeWithCookie(); void TestHandshakeWithCookieButNotAllowed(); void TestHSTSUpgrade(); void TestInvalidSendData(); void TestConnectByWebSocket(ThrottlingOption throttling); void TestConnectBySpdy(SpdyOption spdy, ThrottlingOption throttling); void TestThrottlingLimit(); SpdyWebSocketTestUtil spdy_util_; StreamType stream_type_; scoped_refptr<MockCookieStore> cookie_store_; scoped_ptr<MockURLRequestContext> context_; scoped_refptr<WebSocketJob> websocket_; scoped_refptr<SocketStream> socket_; scoped_ptr<MockClientSocketFactory> socket_factory_; scoped_ptr<OrderedSocketData> data_; TestCompletionCallback sync_test_callback_; scoped_refptr<MockSSLConfigService> ssl_config_service_; scoped_ptr<ProxyService> proxy_service_; scoped_ptr<MockHostResolver> host_resolver_; scoped_ptr<MockHttpTransactionFactory> http_factory_; // Must be set before call to enable_websocket_over_spdy, defaults to false. bool enable_websocket_over_spdy_; static const char kHandshakeRequestWithoutCookie[]; static const char kHandshakeRequestWithCookie[]; static const char kHandshakeRequestWithFilteredCookie[]; static const char kHandshakeResponseWithoutCookie[]; static const char kHandshakeResponseWithCookie[]; static const char kDataHello[]; static const char kDataWorld[]; static const char* const kHandshakeRequestForSpdy[]; static const char* const kHandshakeResponseForSpdy[]; static const size_t kHandshakeRequestWithoutCookieLength; static const size_t kHandshakeRequestWithCookieLength; static const size_t kHandshakeRequestWithFilteredCookieLength; static const size_t kHandshakeResponseWithoutCookieLength; static const size_t kHandshakeResponseWithCookieLength; static const size_t kDataHelloLength; static const size_t kDataWorldLength; }; // Tests using this fixture verify that the WebSocketJob can handle being // deleted while calling back to the delegate correctly. These tests need to be // run under AddressSanitizer or other systems for detecting use-after-free // errors in order to find problems. class WebSocketJobDeleteTest : public ::testing::Test { protected: WebSocketJobDeleteTest() : delegate_(new DeletingSocketStreamDelegate), cookie_store_(new MockCookieStore), context_(new MockURLRequestContext(cookie_store_.get())) { WebSocketJob* websocket = new WebSocketJob(delegate_.get()); delegate_->set_job(websocket); socket_ = new MockSocketStream( GURL("ws://127.0.0.1/"), websocket, context_.get(), NULL); websocket->InitSocketStream(socket_.get()); } void SetDeleteNext() { return delegate_->set_delete_next(true); } WebSocketJob* job() { return delegate_->job(); } scoped_ptr<DeletingSocketStreamDelegate> delegate_; scoped_refptr<MockCookieStore> cookie_store_; scoped_ptr<MockURLRequestContext> context_; scoped_refptr<SocketStream> socket_; }; const char WebSocketJobTest::kHandshakeRequestWithoutCookie[] = "GET /demo HTTP/1.1\r\n" "Host: example.com\r\n" "Upgrade: WebSocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Origin: http://example.com\r\n" "Sec-WebSocket-Protocol: sample\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n"; const char WebSocketJobTest::kHandshakeRequestWithCookie[] = "GET /demo HTTP/1.1\r\n" "Host: example.com\r\n" "Upgrade: WebSocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Origin: http://example.com\r\n" "Sec-WebSocket-Protocol: sample\r\n" "Sec-WebSocket-Version: 13\r\n" "Cookie: WK-test=1\r\n" "\r\n"; const char WebSocketJobTest::kHandshakeRequestWithFilteredCookie[] = "GET /demo HTTP/1.1\r\n" "Host: example.com\r\n" "Upgrade: WebSocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Origin: http://example.com\r\n" "Sec-WebSocket-Protocol: sample\r\n" "Sec-WebSocket-Version: 13\r\n" "Cookie: CR-test=1; CR-test-httponly=1\r\n" "\r\n"; const char WebSocketJobTest::kHandshakeResponseWithoutCookie[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Sec-WebSocket-Protocol: sample\r\n" "\r\n"; const char WebSocketJobTest::kHandshakeResponseWithCookie[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "Sec-WebSocket-Protocol: sample\r\n" "Set-Cookie: CR-set-test=1\r\n" "\r\n"; const char WebSocketJobTest::kDataHello[] = "Hello, "; const char WebSocketJobTest::kDataWorld[] = "World!\n"; const size_t WebSocketJobTest::kHandshakeRequestWithoutCookieLength = arraysize(kHandshakeRequestWithoutCookie) - 1; const size_t WebSocketJobTest::kHandshakeRequestWithCookieLength = arraysize(kHandshakeRequestWithCookie) - 1; const size_t WebSocketJobTest::kHandshakeRequestWithFilteredCookieLength = arraysize(kHandshakeRequestWithFilteredCookie) - 1; const size_t WebSocketJobTest::kHandshakeResponseWithoutCookieLength = arraysize(kHandshakeResponseWithoutCookie) - 1; const size_t WebSocketJobTest::kHandshakeResponseWithCookieLength = arraysize(kHandshakeResponseWithCookie) - 1; const size_t WebSocketJobTest::kDataHelloLength = arraysize(kDataHello) - 1; const size_t WebSocketJobTest::kDataWorldLength = arraysize(kDataWorld) - 1; void WebSocketJobTest::TestSimpleHandshake() { GURL url("ws://example.com/demo"); MockSocketStreamDelegate delegate; InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); DoSendRequest(); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithoutCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithoutCookieLength); EXPECT_EQ(kHandshakeRequestWithoutCookieLength, delegate.amount_sent()); websocket_->OnReceivedData(socket_.get(), kHandshakeResponseWithoutCookie, kHandshakeResponseWithoutCookieLength); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeResponseWithoutCookie, delegate.received_data()); EXPECT_EQ(WebSocketJob::OPEN, GetWebSocketJobState()); CloseWebSocketJob(); } void WebSocketJobTest::TestSlowHandshake() { GURL url("ws://example.com/demo"); MockSocketStreamDelegate delegate; InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); DoSendRequest(); // We assume request is sent in one data chunk (from WebKit) // We don't support streaming request. base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithoutCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithoutCookieLength); EXPECT_EQ(kHandshakeRequestWithoutCookieLength, delegate.amount_sent()); std::vector<std::string> lines; base::SplitString(kHandshakeResponseWithoutCookie, '\n', &lines); for (size_t i = 0; i < lines.size() - 2; i++) { std::string line = lines[i] + "\r\n"; SCOPED_TRACE("Line: " + line); websocket_->OnReceivedData(socket_.get(), line.c_str(), line.size()); base::MessageLoop::current()->RunUntilIdle(); EXPECT_TRUE(delegate.received_data().empty()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); } websocket_->OnReceivedData(socket_.get(), "\r\n", 2); base::MessageLoop::current()->RunUntilIdle(); EXPECT_FALSE(delegate.received_data().empty()); EXPECT_EQ(kHandshakeResponseWithoutCookie, delegate.received_data()); EXPECT_EQ(WebSocketJob::OPEN, GetWebSocketJobState()); CloseWebSocketJob(); } INSTANTIATE_TEST_CASE_P( NextProto, WebSocketJobTest, testing::Values(kProtoDeprecatedSPDY2, kProtoSPDY3, kProtoSPDY31, kProtoSPDY4)); TEST_P(WebSocketJobTest, DelayedCookies) { enable_websocket_over_spdy_ = true; GURL url("ws://example.com/demo"); GURL cookieUrl("http://example.com/demo"); CookieOptions cookie_options; scoped_refptr<DelayedCookieMonster> cookie_store = new DelayedCookieMonster(); context_->set_cookie_store(cookie_store.get()); cookie_store->SetCookieWithOptionsAsync(cookieUrl, "CR-test=1", cookie_options, CookieMonster::SetCookiesCallback()); cookie_options.set_include_httponly(); cookie_store->SetCookieWithOptionsAsync( cookieUrl, "CR-test-httponly=1", cookie_options, CookieMonster::SetCookiesCallback()); MockSocketStreamDelegate delegate; InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); bool sent = websocket_->SendData(kHandshakeRequestWithCookie, kHandshakeRequestWithCookieLength); EXPECT_TRUE(sent); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithFilteredCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithFilteredCookieLength); EXPECT_EQ(kHandshakeRequestWithCookieLength, delegate.amount_sent()); websocket_->OnReceivedData(socket_.get(), kHandshakeResponseWithCookie, kHandshakeResponseWithCookieLength); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeResponseWithoutCookie, delegate.received_data()); EXPECT_EQ(WebSocketJob::OPEN, GetWebSocketJobState()); CloseWebSocketJob(); } void WebSocketJobTest::TestHandshakeWithCookie() { GURL url("ws://example.com/demo"); GURL cookieUrl("http://example.com/demo"); CookieOptions cookie_options; cookie_store_->SetCookieWithOptions( cookieUrl, "CR-test=1", cookie_options); cookie_options.set_include_httponly(); cookie_store_->SetCookieWithOptions( cookieUrl, "CR-test-httponly=1", cookie_options); MockSocketStreamDelegate delegate; InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); bool sent = websocket_->SendData(kHandshakeRequestWithCookie, kHandshakeRequestWithCookieLength); EXPECT_TRUE(sent); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithFilteredCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithFilteredCookieLength); EXPECT_EQ(kHandshakeRequestWithCookieLength, delegate.amount_sent()); websocket_->OnReceivedData(socket_.get(), kHandshakeResponseWithCookie, kHandshakeResponseWithCookieLength); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeResponseWithoutCookie, delegate.received_data()); EXPECT_EQ(WebSocketJob::OPEN, GetWebSocketJobState()); EXPECT_EQ(3U, cookie_store_->entries().size()); EXPECT_EQ(cookieUrl, cookie_store_->entries()[0].url); EXPECT_EQ("CR-test=1", cookie_store_->entries()[0].cookie_line); EXPECT_EQ(cookieUrl, cookie_store_->entries()[1].url); EXPECT_EQ("CR-test-httponly=1", cookie_store_->entries()[1].cookie_line); EXPECT_EQ(cookieUrl, cookie_store_->entries()[2].url); EXPECT_EQ("CR-set-test=1", cookie_store_->entries()[2].cookie_line); CloseWebSocketJob(); } void WebSocketJobTest::TestHandshakeWithCookieButNotAllowed() { GURL url("ws://example.com/demo"); GURL cookieUrl("http://example.com/demo"); CookieOptions cookie_options; cookie_store_->SetCookieWithOptions( cookieUrl, "CR-test=1", cookie_options); cookie_options.set_include_httponly(); cookie_store_->SetCookieWithOptions( cookieUrl, "CR-test-httponly=1", cookie_options); MockSocketStreamDelegate delegate; delegate.set_allow_all_cookies(false); InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); bool sent = websocket_->SendData(kHandshakeRequestWithCookie, kHandshakeRequestWithCookieLength); EXPECT_TRUE(sent); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithoutCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithoutCookieLength); EXPECT_EQ(kHandshakeRequestWithCookieLength, delegate.amount_sent()); websocket_->OnReceivedData(socket_.get(), kHandshakeResponseWithCookie, kHandshakeResponseWithCookieLength); base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeResponseWithoutCookie, delegate.received_data()); EXPECT_EQ(WebSocketJob::OPEN, GetWebSocketJobState()); EXPECT_EQ(2U, cookie_store_->entries().size()); EXPECT_EQ(cookieUrl, cookie_store_->entries()[0].url); EXPECT_EQ("CR-test=1", cookie_store_->entries()[0].cookie_line); EXPECT_EQ(cookieUrl, cookie_store_->entries()[1].url); EXPECT_EQ("CR-test-httponly=1", cookie_store_->entries()[1].cookie_line); CloseWebSocketJob(); } void WebSocketJobTest::TestHSTSUpgrade() { GURL url("ws://upgrademe.com/"); MockSocketStreamDelegate delegate; scoped_refptr<SocketStreamJob> job = SocketStreamJob::CreateSocketStreamJob( url, &delegate, context_->transport_security_state(), context_->ssl_config_service(), NULL, NULL); EXPECT_TRUE(GetSocket(job.get())->is_secure()); job->DetachDelegate(); url = GURL("ws://donotupgrademe.com/"); job = SocketStreamJob::CreateSocketStreamJob( url, &delegate, context_->transport_security_state(), context_->ssl_config_service(), NULL, NULL); EXPECT_FALSE(GetSocket(job.get())->is_secure()); job->DetachDelegate(); } void WebSocketJobTest::TestInvalidSendData() { GURL url("ws://example.com/demo"); MockSocketStreamDelegate delegate; InitWebSocketJob(url, &delegate, STREAM_MOCK_SOCKET); SkipToConnecting(); DoSendRequest(); // We assume request is sent in one data chunk (from WebKit) // We don't support streaming request. base::MessageLoop::current()->RunUntilIdle(); EXPECT_EQ(kHandshakeRequestWithoutCookie, sent_data()); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); websocket_->OnSentData(socket_.get(), kHandshakeRequestWithoutCookieLength); EXPECT_EQ(kHandshakeRequestWithoutCookieLength, delegate.amount_sent()); // We could not send any data until connection is established. bool sent = websocket_->SendData(kHandshakeRequestWithoutCookie, kHandshakeRequestWithoutCookieLength); EXPECT_FALSE(sent); EXPECT_EQ(WebSocketJob::CONNECTING, GetWebSocketJobState()); CloseWebSocketJob(); } // Following tests verify cooperation between WebSocketJob and SocketStream. // Other former tests use MockSocketStream as SocketStream, so we could not // check SocketStream behavior. // OrderedSocketData provide socket level verifiation by checking out-going // packets in comparison with the MockWrite array and emulating in-coming // packets with MockRead array. void WebSocketJobTest::TestConnectByWebSocket( ThrottlingOption throttling) { // This is a test for verifying cooperation between WebSocketJob and // SocketStream. If |throttling| was |THROTTLING_OFF|, it test basic // situation. If |throttling| was |THROTTLING_ON|, throttling limits the // latter connection. MockWrite writes[] = { MockWrite(ASYNC, kHandshakeRequestWithoutCookie, kHandshakeRequestWithoutCookieLength, 1), MockWrite(ASYNC, kDataHello, kDataHelloLength, 3) }; MockRead reads[] = { MockRead(ASYNC, kHandshakeResponseWithoutCookie, kHandshakeResponseWithoutCookieLength, 2), MockRead(ASYNC, kDataWorld, kDataWorldLength, 4), MockRead(SYNCHRONOUS, 0, 5) // EOF }; data_.reset(new OrderedSocketData( reads, arraysize(reads), writes, arraysize(writes))); GURL url("ws://example.com/demo"); MockSocketStreamDelegate delegate; WebSocketJobTest* test = this; if (throttling == THROTTLING_ON) delegate.SetOnStartOpenConnection( base::Bind(&WebSocketJobTest::DoSync, base::Unretained(test))); delegate.SetOnConnected( base::Bind(&WebSocketJobTest::DoSendRequest, base::Unretained(test))); delegate.SetOnReceivedData( base::Bind(&WebSocketJobTest::DoSendData, base::Unretained(test))); delegate.SetOnClose( base::Bind(&WebSocketJobTest::DoSync, base::Unretained(test))); InitWebSocketJob(url, &delegate, STREAM_SOCKET); scoped_refptr<WebSocketJob> block_websocket; if (throttling == THROTTLING_ON) { // Create former WebSocket object which obstructs the latter one. block_websocket = new WebSocketJob(NULL); block_websocket->addresses_ = AddressList(websocket_->address_list()); ASSERT_TRUE( WebSocketThrottle::GetInstance()->PutInQueue(block_websocket.get())); } websocket_->Connect(); if (throttling == THROTTLING_ON) { EXPECT_EQ(OK, WaitForResult()); EXPECT_TRUE(websocket_->IsWaiting()); // Remove the former WebSocket object from throttling queue to unblock the // latter. block_websocket->state_ = WebSocketJob::CLOSED; WebSocketThrottle::GetInstance()->RemoveFromQueue(block_websocket.get()); block_websocket = NULL; } EXPECT_EQ(OK, WaitForResult()); EXPECT_TRUE(data_->at_read_eof()); EXPECT_TRUE(data_->at_write_eof()); EXPECT_EQ(WebSocketJob::CLOSED, GetWebSocketJobState()); } void WebSocketJobTest::TestConnectBySpdy( SpdyOption spdy, ThrottlingOption throttling) { // This is a test for verifying cooperation between WebSocketJob and // SocketStream in the situation we have SPDY session to the server. If // |throttling| was |THROTTLING_ON|, throttling limits the latter connection. // If you enabled spdy, you should specify |spdy| as |SPDY_ON|. Expected // results depend on its configuration. MockWrite writes_websocket[] = { MockWrite(ASYNC, kHandshakeRequestWithoutCookie, kHandshakeRequestWithoutCookieLength, 1), MockWrite(ASYNC, kDataHello, kDataHelloLength, 3) }; MockRead reads_websocket[] = { MockRead(ASYNC, kHandshakeResponseWithoutCookie, kHandshakeResponseWithoutCookieLength, 2), MockRead(ASYNC, kDataWorld, kDataWorldLength, 4), MockRead(SYNCHRONOUS, 0, 5) // EOF }; scoped_ptr<SpdyHeaderBlock> request_headers(new SpdyHeaderBlock()); spdy_util_.SetHeader("path", "/demo", request_headers.get()); spdy_util_.SetHeader("version", "WebSocket/13", request_headers.get()); spdy_util_.SetHeader("scheme", "ws", request_headers.get()); spdy_util_.SetHeader("host", "example.com", request_headers.get()); spdy_util_.SetHeader("origin", "http://example.com", request_headers.get()); spdy_util_.SetHeader("sec-websocket-protocol", "sample", request_headers.get()); scoped_ptr<SpdyHeaderBlock> response_headers(new SpdyHeaderBlock()); spdy_util_.SetHeader("status", "101 Switching Protocols", response_headers.get()); spdy_util_.SetHeader("sec-websocket-protocol", "sample", response_headers.get()); const SpdyStreamId kStreamId = 1; scoped_ptr<SpdyFrame> request_frame( spdy_util_.ConstructSpdyWebSocketHandshakeRequestFrame( request_headers.Pass(), kStreamId, MEDIUM)); scoped_ptr<SpdyFrame> response_frame( spdy_util_.ConstructSpdyWebSocketHandshakeResponseFrame( response_headers.Pass(), kStreamId, MEDIUM)); scoped_ptr<SpdyFrame> data_hello_frame( spdy_util_.ConstructSpdyWebSocketDataFrame( kDataHello, kDataHelloLength, kStreamId, false)); scoped_ptr<SpdyFrame> data_world_frame( spdy_util_.ConstructSpdyWebSocketDataFrame( kDataWorld, kDataWorldLength, kStreamId, false)); MockWrite writes_spdy[] = { CreateMockWrite(*request_frame.get(), 1), CreateMockWrite(*data_hello_frame.get(), 3), }; MockRead reads_spdy[] = { CreateMockRead(*response_frame.get(), 2), CreateMockRead(*data_world_frame.get(), 4), MockRead(SYNCHRONOUS, 0, 5) // EOF }; if (spdy == SPDY_ON) data_.reset(new OrderedSocketData( reads_spdy, arraysize(reads_spdy), writes_spdy, arraysize(writes_spdy))); else data_.reset(new OrderedSocketData( reads_websocket, arraysize(reads_websocket), writes_websocket, arraysize(writes_websocket))); GURL url("ws://example.com/demo"); MockSocketStreamDelegate delegate; WebSocketJobTest* test = this; if (throttling == THROTTLING_ON) delegate.SetOnStartOpenConnection( base::Bind(&WebSocketJobTest::DoSync, base::Unretained(test))); delegate.SetOnConnected( base::Bind(&WebSocketJobTest::DoSendRequest, base::Unretained(test))); delegate.SetOnReceivedData( base::Bind(&WebSocketJobTest::DoSendData, base::Unretained(test))); delegate.SetOnClose( base::Bind(&WebSocketJobTest::DoSync, base::Unretained(test))); InitWebSocketJob(url, &delegate, STREAM_SPDY_WEBSOCKET); scoped_refptr<WebSocketJob> block_websocket; if (throttling == THROTTLING_ON) { // Create former WebSocket object which obstructs the latter one. block_websocket = new WebSocketJob(NULL); block_websocket->addresses_ = AddressList(websocket_->address_list()); ASSERT_TRUE( WebSocketThrottle::GetInstance()->PutInQueue(block_websocket.get())); } websocket_->Connect(); if (throttling == THROTTLING_ON) { EXPECT_EQ(OK, WaitForResult()); EXPECT_TRUE(websocket_->IsWaiting()); // Remove the former WebSocket object from throttling queue to unblock the // latter. block_websocket->state_ = WebSocketJob::CLOSED; WebSocketThrottle::GetInstance()->RemoveFromQueue(block_websocket.get()); block_websocket = NULL; } EXPECT_EQ(OK, WaitForResult()); EXPECT_TRUE(data_->at_read_eof()); EXPECT_TRUE(data_->at_write_eof()); EXPECT_EQ(WebSocketJob::CLOSED, GetWebSocketJobState()); } void WebSocketJobTest::TestThrottlingLimit() { std::vector<scoped_refptr<WebSocketJob> > jobs; const int kMaxWebSocketJobsThrottled = 1024; IPAddressNumber ip; ParseIPLiteralToNumber("127.0.0.1", &ip); for (int i = 0; i < kMaxWebSocketJobsThrottled + 1; ++i) { scoped_refptr<WebSocketJob> job = new WebSocketJob(NULL); job->addresses_ = AddressList(AddressList::CreateFromIPAddress(ip, 80)); if (i >= kMaxWebSocketJobsThrottled) EXPECT_FALSE(WebSocketThrottle::GetInstance()->PutInQueue(job)); else EXPECT_TRUE(WebSocketThrottle::GetInstance()->PutInQueue(job)); jobs.push_back(job); } // Close the jobs in reverse order. Otherwise, We need to make them prepared // for Wakeup call. for (std::vector<scoped_refptr<WebSocketJob> >::reverse_iterator iter = jobs.rbegin(); iter != jobs.rend(); ++iter) { WebSocketJob* job = (*iter).get(); job->state_ = WebSocketJob::CLOSED; WebSocketThrottle::GetInstance()->RemoveFromQueue(job); } } // Execute tests in both spdy-disabled mode and spdy-enabled mode. TEST_P(WebSocketJobTest, SimpleHandshake) { TestSimpleHandshake(); } TEST_P(WebSocketJobTest, SlowHandshake) { TestSlowHandshake(); } TEST_P(WebSocketJobTest, HandshakeWithCookie) { TestHandshakeWithCookie(); } TEST_P(WebSocketJobTest, HandshakeWithCookieButNotAllowed) { TestHandshakeWithCookieButNotAllowed(); } TEST_P(WebSocketJobTest, HSTSUpgrade) { TestHSTSUpgrade(); } TEST_P(WebSocketJobTest, InvalidSendData) { TestInvalidSendData(); } TEST_P(WebSocketJobTest, SimpleHandshakeSpdyEnabled) { enable_websocket_over_spdy_ = true; TestSimpleHandshake(); } TEST_P(WebSocketJobTest, SlowHandshakeSpdyEnabled) { enable_websocket_over_spdy_ = true; TestSlowHandshake(); } TEST_P(WebSocketJobTest, HandshakeWithCookieSpdyEnabled) { enable_websocket_over_spdy_ = true; TestHandshakeWithCookie(); } TEST_P(WebSocketJobTest, HandshakeWithCookieButNotAllowedSpdyEnabled) { enable_websocket_over_spdy_ = true; TestHandshakeWithCookieButNotAllowed(); } TEST_P(WebSocketJobTest, HSTSUpgradeSpdyEnabled) { enable_websocket_over_spdy_ = true; TestHSTSUpgrade(); } TEST_P(WebSocketJobTest, InvalidSendDataSpdyEnabled) { enable_websocket_over_spdy_ = true; TestInvalidSendData(); } TEST_P(WebSocketJobTest, ConnectByWebSocket) { enable_websocket_over_spdy_ = true; TestConnectByWebSocket(THROTTLING_OFF); } TEST_P(WebSocketJobTest, ConnectByWebSocketSpdyEnabled) { enable_websocket_over_spdy_ = true; TestConnectByWebSocket(THROTTLING_OFF); } TEST_P(WebSocketJobTest, ConnectBySpdy) { TestConnectBySpdy(SPDY_OFF, THROTTLING_OFF); } TEST_P(WebSocketJobTest, ConnectBySpdySpdyEnabled) { enable_websocket_over_spdy_ = true; TestConnectBySpdy(SPDY_ON, THROTTLING_OFF); } TEST_P(WebSocketJobTest, ThrottlingWebSocket) { TestConnectByWebSocket(THROTTLING_ON); } TEST_P(WebSocketJobTest, ThrottlingMaxNumberOfThrottledJobLimit) { TestThrottlingLimit(); } TEST_P(WebSocketJobTest, ThrottlingWebSocketSpdyEnabled) { enable_websocket_over_spdy_ = true; TestConnectByWebSocket(THROTTLING_ON); } TEST_P(WebSocketJobTest, ThrottlingSpdy) { TestConnectBySpdy(SPDY_OFF, THROTTLING_ON); } TEST_P(WebSocketJobTest, ThrottlingSpdySpdyEnabled) { enable_websocket_over_spdy_ = true; TestConnectBySpdy(SPDY_ON, THROTTLING_ON); } TEST_F(WebSocketJobDeleteTest, OnClose) { SetDeleteNext(); job()->OnClose(socket_.get()); // OnClose() sets WebSocketJob::_socket to NULL before we can detach it, so // socket_->delegate is still set at this point. Clear it to avoid hitting // DCHECK(!delegate_) in the SocketStream destructor. SocketStream::Finish() // is the only caller of this method in real code, and it also sets delegate_ // to NULL. socket_->DetachDelegate(); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, OnAuthRequired) { SetDeleteNext(); job()->OnAuthRequired(socket_.get(), NULL); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, OnSSLCertificateError) { SSLInfo ssl_info; SetDeleteNext(); job()->OnSSLCertificateError(socket_.get(), ssl_info, true); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, OnError) { SetDeleteNext(); job()->OnError(socket_.get(), ERR_CONNECTION_RESET); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, OnSentSpdyHeaders) { job()->Connect(); SetDeleteNext(); job()->OnSentSpdyHeaders(); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, OnSentHandshakeRequest) { static const char kMinimalRequest[] = "GET /demo HTTP/1.1\r\n" "Host: example.com\r\n" "Upgrade: WebSocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Origin: http://example.com\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n"; const size_t kMinimalRequestSize = arraysize(kMinimalRequest) - 1; job()->Connect(); job()->SendData(kMinimalRequest, kMinimalRequestSize); SetDeleteNext(); job()->OnSentData(socket_.get(), kMinimalRequestSize); EXPECT_FALSE(job()); } TEST_F(WebSocketJobDeleteTest, NotifyHeadersComplete) { static const char kMinimalResponse[] = "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" "\r\n"; job()->Connect(); SetDeleteNext(); job()->OnReceivedData( socket_.get(), kMinimalResponse, arraysize(kMinimalResponse) - 1); EXPECT_FALSE(job()); } // TODO(toyoshim): Add tests to verify throttling, SPDY stream limitation. // TODO(toyoshim,yutak): Add tests to verify closing handshake. } // namespace net