WebSocketBasicStream is the basic implementation of the WebSocket protocol over
TCP/IP with no extensions in use.
This CL implements the logic to read and write frames from the stream that is used after connection.
BUG=257680
TEST=net_unittests
Review URL: https://chromiumcodereview.appspot.com/18792002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@220573 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/net/net.gyp b/net/net.gyp
index a189432..d297e26 100644
--- a/net/net.gyp
+++ b/net/net.gyp
@@ -1089,6 +1089,8 @@
'url_request/url_request_throttler_manager.h',
'url_request/view_cache_helper.cc',
'url_request/view_cache_helper.h',
+ 'websockets/websocket_basic_stream.cc',
+ 'websockets/websocket_basic_stream.h',
'websockets/websocket_channel.cc',
'websockets/websocket_channel.h',
'websockets/websocket_errors.cc',
@@ -1857,6 +1859,7 @@
'url_request/url_request_throttler_unittest.cc',
'url_request/url_request_unittest.cc',
'url_request/view_cache_helper_unittest.cc',
+ 'websockets/websocket_basic_stream_test.cc',
'websockets/websocket_channel_test.cc',
'websockets/websocket_errors_unittest.cc',
'websockets/websocket_frame_parser_unittest.cc',
diff --git a/net/websockets/README b/net/websockets/README
index 558a4511..1e01f61 100644
--- a/net/websockets/README
+++ b/net/websockets/README
@@ -30,6 +30,9 @@
presents a high-level interface to the renderer process similar to a
multiplexing proxy. This is not yet used in any stable Chromium version.
+websocket_basic_stream.cc
+websocket_basic_stream.h
+websocket_basic_stream_test.cc
websocket_channel.cc
websocket_channel.h
websocket_channel_test.cc
diff --git a/net/websockets/websocket_basic_stream.cc b/net/websockets/websocket_basic_stream.cc
new file mode 100644
index 0000000..5b02b18
--- /dev/null
+++ b/net/websockets/websocket_basic_stream.cc
@@ -0,0 +1,259 @@
+// 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_basic_stream.h"
+
+#include <algorithm>
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/bind.h"
+#include "base/logging.h"
+#include "net/base/io_buffer.h"
+#include "net/base/net_errors.h"
+#include "net/socket/client_socket_handle.h"
+#include "net/websockets/websocket_errors.h"
+#include "net/websockets/websocket_frame.h"
+#include "net/websockets/websocket_frame_parser.h"
+
+namespace net {
+
+namespace {
+
+// The number of bytes to attempt to read at a time.
+// TODO(ricea): See if there is a better number or algorithm to fulfill our
+// requirements:
+// 1. We would like to use minimal memory on low-bandwidth or idle connections
+// 2. We would like to read as close to line speed as possible on
+// high-bandwidth connections
+// 3. We can't afford to cause jank on the IO thread by copying large buffers
+// around
+// 4. We would like to hit any sweet-spots that might exist in terms of network
+// packet sizes / encryption block sizes / IPC alignment issues, etc.
+const int kReadBufferSize = 32 * 1024;
+
+} // namespace
+
+WebSocketBasicStream::WebSocketBasicStream(
+ scoped_ptr<ClientSocketHandle> connection)
+ : read_buffer_(new IOBufferWithSize(kReadBufferSize)),
+ connection_(connection.Pass()),
+ generate_websocket_masking_key_(&GenerateWebSocketMaskingKey) {
+ DCHECK(connection_->is_initialized());
+}
+
+WebSocketBasicStream::~WebSocketBasicStream() { Close(); }
+
+int WebSocketBasicStream::ReadFrames(
+ ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback) {
+ DCHECK(frame_chunks->empty());
+ // If there is data left over after parsing the HTTP headers, attempt to parse
+ // it as WebSocket frames.
+ if (http_read_buffer_) {
+ DCHECK_GE(http_read_buffer_->offset(), 0);
+ // We cannot simply copy the data into read_buffer_, as it might be too
+ // large.
+ scoped_refptr<GrowableIOBuffer> buffered_data;
+ buffered_data.swap(http_read_buffer_);
+ DCHECK(http_read_buffer_.get() == NULL);
+ if (!parser_.Decode(buffered_data->StartOfBuffer(),
+ buffered_data->offset(),
+ frame_chunks))
+ return WebSocketErrorToNetError(parser_.websocket_error());
+ if (!frame_chunks->empty())
+ return OK;
+ }
+
+ // Run until socket stops giving us data or we get some chunks.
+ while (true) {
+ // base::Unretained(this) here is safe because net::Socket guarantees not to
+ // call any callbacks after Disconnect(), which we call from the
+ // destructor. The caller of ReadFrames() is required to keep |frame_chunks|
+ // valid.
+ int result = connection_->socket()
+ ->Read(read_buffer_.get(),
+ read_buffer_->size(),
+ base::Bind(&WebSocketBasicStream::OnReadComplete,
+ base::Unretained(this),
+ base::Unretained(frame_chunks),
+ callback));
+ if (result == ERR_IO_PENDING)
+ return result;
+ result = HandleReadResult(result, frame_chunks);
+ if (result != ERR_IO_PENDING)
+ return result;
+ }
+}
+
+int WebSocketBasicStream::WriteFrames(
+ ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback) {
+ // This function always concatenates all frames into a single buffer.
+ // TODO(ricea): Investigate whether it would be better in some cases to
+ // perform multiple writes with smaller buffers.
+ //
+ // First calculate the size of the buffer we need to allocate.
+ typedef ScopedVector<WebSocketFrameChunk>::const_iterator Iterator;
+ const int kMaximumTotalSize = std::numeric_limits<int>::max();
+ int total_size = 0;
+ for (Iterator it = frame_chunks->begin(); it != frame_chunks->end(); ++it) {
+ WebSocketFrameChunk* chunk = *it;
+ DCHECK(chunk->header)
+ << "Only complete frames are supported by WebSocketBasicStream";
+ DCHECK(chunk->final_chunk)
+ << "Only complete frames are supported by WebSocketBasicStream";
+ // Force the masked bit on.
+ chunk->header->masked = true;
+ // We enforce flow control so the renderer should never be able to force us
+ // to cache anywhere near 2GB of frames.
+ int chunk_size =
+ chunk->data->size() + GetWebSocketFrameHeaderSize(*(chunk->header));
+ CHECK_GE(kMaximumTotalSize - total_size, chunk_size)
+ << "Aborting to prevent overflow";
+ total_size += chunk_size;
+ }
+ scoped_refptr<IOBufferWithSize> combined_buffer(
+ new IOBufferWithSize(total_size));
+ char* dest = combined_buffer->data();
+ int remaining_size = total_size;
+ for (Iterator it = frame_chunks->begin(); it != frame_chunks->end(); ++it) {
+ WebSocketFrameChunk* chunk = *it;
+ WebSocketMaskingKey mask = generate_websocket_masking_key_();
+ int result = WriteWebSocketFrameHeader(
+ *(chunk->header), &mask, dest, remaining_size);
+ DCHECK(result != ERR_INVALID_ARGUMENT)
+ << "WriteWebSocketFrameHeader() says that " << remaining_size
+ << " is not enough to write the header in. This should not happen.";
+ CHECK_GE(result, 0) << "Potentially security-critical check failed";
+ dest += result;
+ remaining_size -= result;
+
+ const char* const frame_data = chunk->data->data();
+ const int frame_size = chunk->data->size();
+ CHECK_GE(remaining_size, frame_size);
+ std::copy(frame_data, frame_data + frame_size, dest);
+ MaskWebSocketFramePayload(mask, 0, dest, frame_size);
+ dest += frame_size;
+ remaining_size -= frame_size;
+ }
+ DCHECK_EQ(0, remaining_size) << "Buffer size calculation was wrong; "
+ << remaining_size << " bytes left over.";
+ scoped_refptr<DrainableIOBuffer> drainable_buffer(
+ new DrainableIOBuffer(combined_buffer, total_size));
+ return WriteEverything(drainable_buffer, callback);
+}
+
+void WebSocketBasicStream::Close() { connection_->socket()->Disconnect(); }
+
+std::string WebSocketBasicStream::GetSubProtocol() const {
+ return sub_protocol_;
+}
+
+std::string WebSocketBasicStream::GetExtensions() const { return extensions_; }
+
+int WebSocketBasicStream::SendHandshakeRequest(
+ const GURL& url,
+ const HttpRequestHeaders& headers,
+ HttpResponseInfo* response_info,
+ const CompletionCallback& callback) {
+ // TODO(ricea): Implement handshake-related functionality.
+ NOTREACHED();
+ return ERR_NOT_IMPLEMENTED;
+}
+
+int WebSocketBasicStream::ReadHandshakeResponse(
+ const CompletionCallback& callback) {
+ NOTREACHED();
+ return ERR_NOT_IMPLEMENTED;
+}
+
+/*static*/
+scoped_ptr<WebSocketBasicStream>
+WebSocketBasicStream::CreateWebSocketBasicStreamForTesting(
+ scoped_ptr<ClientSocketHandle> connection,
+ const scoped_refptr<GrowableIOBuffer>& http_read_buffer,
+ const std::string& sub_protocol,
+ const std::string& extensions,
+ WebSocketMaskingKeyGeneratorFunction key_generator_function) {
+ scoped_ptr<WebSocketBasicStream> stream(
+ new WebSocketBasicStream(connection.Pass()));
+ if (http_read_buffer) {
+ stream->http_read_buffer_ = http_read_buffer;
+ }
+ stream->sub_protocol_ = sub_protocol;
+ stream->extensions_ = extensions;
+ stream->generate_websocket_masking_key_ = key_generator_function;
+ return stream.Pass();
+}
+
+int WebSocketBasicStream::WriteEverything(
+ const scoped_refptr<DrainableIOBuffer>& buffer,
+ const CompletionCallback& callback) {
+ while (buffer->BytesRemaining() > 0) {
+ // The use of base::Unretained() here is safe because on destruction we
+ // disconnect the socket, preventing any further callbacks.
+ int result = connection_->socket()
+ ->Write(buffer.get(),
+ buffer->BytesRemaining(),
+ base::Bind(&WebSocketBasicStream::OnWriteComplete,
+ base::Unretained(this),
+ buffer,
+ callback));
+ if (result > 0) {
+ buffer->DidConsume(result);
+ } else {
+ return result;
+ }
+ }
+ return OK;
+}
+
+void WebSocketBasicStream::OnWriteComplete(
+ const scoped_refptr<DrainableIOBuffer>& buffer,
+ const CompletionCallback& callback,
+ int result) {
+ if (result < 0) {
+ DCHECK(result != ERR_IO_PENDING);
+ callback.Run(result);
+ return;
+ }
+
+ DCHECK(result != 0);
+ buffer->DidConsume(result);
+ result = WriteEverything(buffer, callback);
+ if (result != ERR_IO_PENDING)
+ callback.Run(result);
+}
+
+int WebSocketBasicStream::HandleReadResult(
+ int result,
+ ScopedVector<WebSocketFrameChunk>* frame_chunks) {
+ DCHECK_NE(ERR_IO_PENDING, result);
+ DCHECK(frame_chunks->empty());
+ if (result < 0)
+ return result;
+ if (result == 0)
+ return ERR_CONNECTION_CLOSED;
+ if (!parser_.Decode(read_buffer_->data(), result, frame_chunks))
+ return WebSocketErrorToNetError(parser_.websocket_error());
+ if (!frame_chunks->empty())
+ return OK;
+ return ERR_IO_PENDING;
+}
+
+void WebSocketBasicStream::OnReadComplete(
+ ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback,
+ int result) {
+ result = HandleReadResult(result, frame_chunks);
+ if (result == ERR_IO_PENDING)
+ result = ReadFrames(frame_chunks, callback);
+ if (result != ERR_IO_PENDING)
+ callback.Run(result);
+}
+
+} // namespace net
diff --git a/net/websockets/websocket_basic_stream.h b/net/websockets/websocket_basic_stream.h
new file mode 100644
index 0000000..1231da8
--- /dev/null
+++ b/net/websockets/websocket_basic_stream.h
@@ -0,0 +1,129 @@
+// 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.
+
+#ifndef NET_WEBSOCKETS_WEBSOCKET_BASIC_STREAM_H_
+#define NET_WEBSOCKETS_WEBSOCKET_BASIC_STREAM_H_
+
+#include <string>
+
+#include "base/callback.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "net/websockets/websocket_frame_parser.h"
+#include "net/websockets/websocket_stream.h"
+
+namespace net {
+
+class ClientSocketHandle;
+class DrainableIOBuffer;
+class GrowableIOBuffer;
+class HttpRequestHeaders;
+class HttpResponseInfo;
+class IOBufferWithSize;
+struct WebSocketFrameChunk;
+
+// Implementation of WebSocketStream for non-multiplexed ws:// connections (or
+// the physical side of a multiplexed ws:// connection).
+class NET_EXPORT_PRIVATE WebSocketBasicStream : public WebSocketStream {
+ public:
+ typedef WebSocketMaskingKey (*WebSocketMaskingKeyGeneratorFunction)();
+
+ // This class should not normally be constructed directly; see
+ // WebSocketStream::CreateAndConnectStream.
+ explicit WebSocketBasicStream(scoped_ptr<ClientSocketHandle> connection);
+
+ // The destructor has to make sure the connection is closed when we finish so
+ // that it does not get returned to the pool.
+ virtual ~WebSocketBasicStream();
+
+ // WebSocketStream implementation.
+ virtual int ReadFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback) OVERRIDE;
+
+ virtual int WriteFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback) OVERRIDE;
+
+ virtual void Close() OVERRIDE;
+
+ virtual std::string GetSubProtocol() const OVERRIDE;
+
+ virtual std::string GetExtensions() const OVERRIDE;
+
+ // Writes WebSocket handshake request HTTP-style to the connection. Adds
+ // "Sec-WebSocket-Key" header; this should not be included in |headers|.
+ virtual int SendHandshakeRequest(const GURL& url,
+ const HttpRequestHeaders& headers,
+ HttpResponseInfo* response_info,
+ const CompletionCallback& callback) OVERRIDE;
+
+ virtual int ReadHandshakeResponse(
+ const CompletionCallback& callback) OVERRIDE;
+
+ ////////////////////////////////////////////////////////////////////////////
+ // Methods for testing only.
+
+ static scoped_ptr<WebSocketBasicStream> CreateWebSocketBasicStreamForTesting(
+ scoped_ptr<ClientSocketHandle> connection,
+ const scoped_refptr<GrowableIOBuffer>& http_read_buffer,
+ const std::string& sub_protocol,
+ const std::string& extensions,
+ WebSocketMaskingKeyGeneratorFunction key_generator_function);
+
+ private:
+ // Returns OK or calls |callback| when the |buffer| is fully drained or
+ // something has failed.
+ int WriteEverything(const scoped_refptr<DrainableIOBuffer>& buffer,
+ const CompletionCallback& callback);
+
+ // Wraps the |callback| to continue writing until everything has been written.
+ void OnWriteComplete(const scoped_refptr<DrainableIOBuffer>& buffer,
+ const CompletionCallback& callback,
+ int result);
+
+ // Attempts to parse the output of a read as WebSocket frames. On success,
+ // returns OK and places the frame(s) in frame_chunks.
+ int HandleReadResult(int result,
+ ScopedVector<WebSocketFrameChunk>* frame_chunks);
+
+ // Called when a read completes. Parses the result and (unless no complete
+ // header has been received) calls |callback|.
+ void OnReadComplete(ScopedVector<WebSocketFrameChunk>* frame_chunks,
+ const CompletionCallback& callback,
+ int result);
+
+ // Storage for pending reads. All active WebSockets spend all the time with a
+ // call to ReadFrames() pending, so there is no benefit in trying to share
+ // this between sockets.
+ scoped_refptr<IOBufferWithSize> read_buffer_;
+
+ // The connection, wrapped in a ClientSocketHandle so that we can prevent it
+ // from being returned to the pool.
+ scoped_ptr<ClientSocketHandle> connection_;
+
+ // Only used during handshake. Some data may be left in this buffer after the
+ // handshake, in which case it will be picked up during the first call to
+ // ReadFrames(). The type is GrowableIOBuffer for compatibility with
+ // net::HttpStreamParser, which is used to parse the handshake.
+ scoped_refptr<GrowableIOBuffer> http_read_buffer_;
+
+ // This keeps the current parse state (including any incomplete headers) and
+ // parses frames.
+ WebSocketFrameParser parser_;
+
+ // The negotated sub-protocol, or empty for none.
+ std::string sub_protocol_;
+
+ // The extensions negotiated with the remote server.
+ std::string extensions_;
+
+ // This can be overridden in tests to make the output deterministic. We don't
+ // use a Callback here because a function pointer is faster and good enough
+ // for our purposes.
+ WebSocketMaskingKeyGeneratorFunction generate_websocket_masking_key_;
+};
+
+} // namespace net
+
+#endif // NET_WEBSOCKETS_WEBSOCKET_BASIC_STREAM_H_
diff --git a/net/websockets/websocket_basic_stream_test.cc b/net/websockets/websocket_basic_stream_test.cc
new file mode 100644
index 0000000..ec2e51a
--- /dev/null
+++ b/net/websockets/websocket_basic_stream_test.cc
@@ -0,0 +1,513 @@
+// 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.
+//
+// Tests for WebSocketBasicStream. Note that we do not attempt to verify that
+// frame parsing itself functions correctly, as that is covered by the
+// WebSocketFrameParser tests.
+
+#include "net/websockets/websocket_basic_stream.h"
+
+#include "base/basictypes.h"
+#include "base/port.h"
+#include "net/base/capturing_net_log.h"
+#include "net/base/test_completion_callback.h"
+#include "net/socket/socket_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+namespace {
+
+// TODO(ricea): Add tests for
+// - Empty frames (data & control)
+// - Non-NULL masking key
+// - A frame larger than kReadBufferSize;
+
+const char kSampleFrame[] = "\x81\x06Sample";
+const size_t kSampleFrameSize = arraysize(kSampleFrame) - 1;
+const char kPartialLargeFrame[] =
+ "\x81\x7F\x00\x00\x00\x00\x7F\xFF\xFF\xFF"
+ "chromiunum ad pasco per loca insanis pullum manducat frumenti";
+const size_t kPartialLargeFrameSize = arraysize(kPartialLargeFrame) - 1;
+const size_t kLargeFrameHeaderSize = 10;
+const size_t kLargeFrameDeclaredPayloadSize = 0x7FFFFFFF;
+const char kMultipleFrames[] = "\x81\x01X\x81\x01Y\x81\x01Z";
+const size_t kMultipleFramesSize = arraysize(kMultipleFrames) - 1;
+// This frame encodes a payload length of 7 in two bytes, which is always
+// invalid.
+const char kInvalidFrame[] = "\x81\x7E\x00\x07Invalid";
+const size_t kInvalidFrameSize = arraysize(kInvalidFrame) - 1;
+const char kWriteFrame[] = "\x81\x85\x00\x00\x00\x00Write";
+const size_t kWriteFrameSize = arraysize(kWriteFrame) - 1;
+const WebSocketMaskingKey kNulMaskingKey = {{'\0', '\0', '\0', '\0'}};
+
+// Generates a ScopedVector<WebSocketFrameChunk> which will have a wire format
+// matching kWriteFrame.
+ScopedVector<WebSocketFrameChunk> GenerateWriteFrame() {
+ scoped_ptr<WebSocketFrameChunk> chunk(new WebSocketFrameChunk);
+ const size_t payload_size =
+ kWriteFrameSize - (WebSocketFrameHeader::kBaseHeaderSize +
+ WebSocketFrameHeader::kMaskingKeyLength);
+ chunk->data = new IOBufferWithSize(payload_size);
+ memcpy(chunk->data->data(),
+ kWriteFrame + kWriteFrameSize - payload_size,
+ payload_size);
+ chunk->final_chunk = true;
+ scoped_ptr<WebSocketFrameHeader> header(
+ new WebSocketFrameHeader(WebSocketFrameHeader::kOpCodeText));
+ header->final = true;
+ header->masked = true;
+ header->payload_length = payload_size;
+ chunk->header = header.Pass();
+ ScopedVector<WebSocketFrameChunk> chunks;
+ chunks.push_back(chunk.release());
+ return chunks.Pass();
+}
+
+// A masking key generator function which generates the identity mask,
+// ie. "\0\0\0\0".
+WebSocketMaskingKey GenerateNulMaskingKey() { return kNulMaskingKey; }
+
+// Base class for WebSocketBasicStream test fixtures.
+class WebSocketBasicStreamTest : public ::testing::Test {
+ protected:
+ scoped_ptr<WebSocketBasicStream> stream_;
+ CapturingNetLog net_log_;
+};
+
+// A fixture for tests which only perform normal socket operations.
+class WebSocketBasicStreamSocketTest : public WebSocketBasicStreamTest {
+ protected:
+ WebSocketBasicStreamSocketTest()
+ : histograms_("a"), pool_(1, 1, &histograms_, &factory_) {}
+
+ virtual ~WebSocketBasicStreamSocketTest() {
+ // stream_ has a reference to socket_data_ (via MockTCPClientSocket) and so
+ // should be destroyed first.
+ stream_.reset();
+ }
+
+ scoped_ptr<ClientSocketHandle> MakeTransportSocket(MockRead reads[],
+ size_t reads_count,
+ MockWrite writes[],
+ size_t writes_count) {
+ socket_data_.reset(
+ new StaticSocketDataProvider(reads, reads_count, writes, writes_count));
+ socket_data_->set_connect_data(MockConnect(SYNCHRONOUS, OK));
+ factory_.AddSocketDataProvider(socket_data_.get());
+
+ scoped_ptr<ClientSocketHandle> transport_socket(new ClientSocketHandle);
+ scoped_refptr<MockTransportSocketParams> params;
+ transport_socket->Init("a",
+ params,
+ MEDIUM,
+ CompletionCallback(),
+ &pool_,
+ bound_net_log_.bound());
+ return transport_socket.Pass();
+ }
+
+ void SetHttpReadBuffer(const char* data, size_t size) {
+ http_read_buffer_ = new GrowableIOBuffer;
+ http_read_buffer_->SetCapacity(size);
+ memcpy(http_read_buffer_->data(), data, size);
+ http_read_buffer_->set_offset(size);
+ }
+
+ void CreateStream(MockRead reads[],
+ size_t reads_count,
+ MockWrite writes[],
+ size_t writes_count) {
+ stream_ = WebSocketBasicStream::CreateWebSocketBasicStreamForTesting(
+ MakeTransportSocket(reads, reads_count, writes, writes_count),
+ http_read_buffer_,
+ sub_protocol_,
+ extensions_,
+ &GenerateNulMaskingKey);
+ }
+
+ template <size_t N>
+ void CreateReadOnly(MockRead (&reads)[N]) {
+ CreateStream(reads, N, NULL, 0);
+ }
+
+ template <size_t N>
+ void CreateWriteOnly(MockWrite (&writes)[N]) {
+ CreateStream(NULL, 0, writes, N);
+ }
+
+ void CreateNullStream() { CreateStream(NULL, 0, NULL, 0); }
+
+ scoped_ptr<SocketDataProvider> socket_data_;
+ MockClientSocketFactory factory_;
+ ClientSocketPoolHistograms histograms_;
+ MockTransportClientSocketPool pool_;
+ CapturingBoundNetLog(bound_net_log_);
+ ScopedVector<WebSocketFrameChunk> frame_chunks_;
+ TestCompletionCallback cb_;
+ scoped_refptr<GrowableIOBuffer> http_read_buffer_;
+ std::string sub_protocol_;
+ std::string extensions_;
+};
+
+TEST_F(WebSocketBasicStreamSocketTest, ConstructionWorks) {
+ CreateNullStream();
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, SyncReadWorks) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, kSampleFrame, kSampleFrameSize)};
+ CreateReadOnly(reads);
+ int result = stream_->ReadFrames(&frame_chunks_, cb_.callback());
+ EXPECT_EQ(OK, result);
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(GG_UINT64_C(6), frame_chunks_[0]->header->payload_length);
+ EXPECT_TRUE(frame_chunks_[0]->header->final);
+ EXPECT_TRUE(frame_chunks_[0]->final_chunk);
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, AsyncReadWorks) {
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame, kSampleFrameSize)};
+ CreateReadOnly(reads);
+ int result = stream_->ReadFrames(&frame_chunks_, cb_.callback());
+ ASSERT_EQ(ERR_IO_PENDING, result);
+ EXPECT_EQ(OK, cb_.WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(GG_UINT64_C(6), frame_chunks_[0]->header->payload_length);
+ // Don't repeat all the tests from SyncReadWorks; just enough to be sure the
+ // frame was really read.
+}
+
+// ReadFrames will not return a frame whose header has not been wholly received.
+TEST_F(WebSocketBasicStreamSocketTest, HeaderFragmentedSync) {
+ MockRead reads[] = {
+ MockRead(SYNCHRONOUS, kSampleFrame, 1),
+ MockRead(SYNCHRONOUS, kSampleFrame + 1, kSampleFrameSize - 1)};
+ CreateReadOnly(reads);
+ int result = stream_->ReadFrames(&frame_chunks_, cb_.callback());
+ ASSERT_EQ(OK, result);
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(GG_UINT64_C(6), frame_chunks_[0]->header->payload_length);
+}
+
+// The same behaviour applies to asynchronous reads.
+TEST_F(WebSocketBasicStreamSocketTest, HeaderFragmentedAsync) {
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame, 1),
+ MockRead(ASYNC, kSampleFrame + 1, kSampleFrameSize - 1)};
+ CreateReadOnly(reads);
+ int result = stream_->ReadFrames(&frame_chunks_, cb_.callback());
+ ASSERT_EQ(ERR_IO_PENDING, result);
+ EXPECT_EQ(OK, cb_.WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(GG_UINT64_C(6), frame_chunks_[0]->header->payload_length);
+}
+
+// If it receives an incomplete header in a synchronous call, then has to wait
+// for the rest of the frame, ReadFrames will return ERR_IO_PENDING.
+TEST_F(WebSocketBasicStreamSocketTest, HeaderFragmentedSyncAsync) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, kSampleFrame, 1),
+ MockRead(ASYNC, kSampleFrame + 1, kSampleFrameSize - 1)};
+ CreateReadOnly(reads);
+ int result = stream_->ReadFrames(&frame_chunks_, cb_.callback());
+ ASSERT_EQ(ERR_IO_PENDING, result);
+ EXPECT_EQ(OK, cb_.WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(GG_UINT64_C(6), frame_chunks_[0]->header->payload_length);
+}
+
+// An extended header should also return ERR_IO_PENDING if it is not completely
+// received.
+TEST_F(WebSocketBasicStreamSocketTest, FragmentedLargeHeader) {
+ MockRead reads[] = {
+ MockRead(SYNCHRONOUS, kPartialLargeFrame, kLargeFrameHeaderSize - 1),
+ MockRead(SYNCHRONOUS, ERR_IO_PENDING)};
+ CreateReadOnly(reads);
+ EXPECT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+// A frame that does not arrive in a single read should arrive in chunks.
+TEST_F(WebSocketBasicStreamSocketTest, LargeFrameFirstChunk) {
+ MockRead reads[] = {
+ MockRead(SYNCHRONOUS, kPartialLargeFrame, kPartialLargeFrameSize)};
+ CreateReadOnly(reads);
+ EXPECT_EQ(OK, stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(kLargeFrameDeclaredPayloadSize,
+ frame_chunks_[0]->header->payload_length);
+ EXPECT_TRUE(frame_chunks_[0]->header->final);
+ EXPECT_FALSE(frame_chunks_[0]->final_chunk);
+ EXPECT_EQ(kPartialLargeFrameSize - kLargeFrameHeaderSize,
+ static_cast<size_t>(frame_chunks_[0]->data->size()));
+}
+
+// If only the header arrives, we should get a zero-byte chunk.
+TEST_F(WebSocketBasicStreamSocketTest, HeaderOnlyChunk) {
+ MockRead reads[] = {
+ MockRead(SYNCHRONOUS, kPartialLargeFrame, kLargeFrameHeaderSize)};
+ CreateReadOnly(reads);
+ EXPECT_EQ(OK, stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(1U, frame_chunks_.size());
+ EXPECT_FALSE(frame_chunks_[0]->final_chunk);
+ EXPECT_TRUE(frame_chunks_[0]->data.get() == NULL);
+}
+
+// The second and subsequent chunks of a frame have no header.
+TEST_F(WebSocketBasicStreamSocketTest, LargeFrameTwoChunks) {
+ static const size_t kChunkSize = 16;
+ MockRead reads[] = {
+ MockRead(ASYNC, kPartialLargeFrame, kChunkSize),
+ MockRead(ASYNC, kPartialLargeFrame + kChunkSize, kChunkSize)};
+ CreateReadOnly(reads);
+ TestCompletionCallback cb[2];
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb[0].callback()));
+ EXPECT_EQ(OK, cb[0].WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+
+ frame_chunks_.clear();
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb[1].callback()));
+ EXPECT_EQ(OK, cb[1].WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_FALSE(frame_chunks_[0]->header);
+}
+
+// Only the final chunk of a frame has final_chunk set.
+TEST_F(WebSocketBasicStreamSocketTest, OnlyFinalChunkIsFinal) {
+ static const size_t kFirstChunkSize = 4;
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame, kFirstChunkSize),
+ MockRead(ASYNC,
+ kSampleFrame + kFirstChunkSize,
+ kSampleFrameSize - kFirstChunkSize)};
+ CreateReadOnly(reads);
+ TestCompletionCallback cb[2];
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb[0].callback()));
+ EXPECT_EQ(OK, cb[0].WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_FALSE(frame_chunks_[0]->final_chunk);
+
+ frame_chunks_.clear();
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb[1].callback()));
+ EXPECT_EQ(OK, cb[1].WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->final_chunk);
+}
+
+// Multiple frames that arrive together should be parsed correctly.
+TEST_F(WebSocketBasicStreamSocketTest, ThreeFramesTogether) {
+ MockRead reads[] = {
+ MockRead(SYNCHRONOUS, kMultipleFrames, kMultipleFramesSize)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(OK, stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(3U, frame_chunks_.size());
+ EXPECT_TRUE(frame_chunks_[0]->final_chunk);
+ EXPECT_TRUE(frame_chunks_[1]->final_chunk);
+ EXPECT_TRUE(frame_chunks_[2]->final_chunk);
+}
+
+// ERR_CONNECTION_CLOSED must be returned on close.
+TEST_F(WebSocketBasicStreamSocketTest, SyncClose) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, "", 0)};
+ CreateReadOnly(reads);
+
+ EXPECT_EQ(ERR_CONNECTION_CLOSED,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, AsyncClose) {
+ MockRead reads[] = {MockRead(ASYNC, "", 0)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(ERR_CONNECTION_CLOSED, cb_.WaitForResult());
+}
+
+// The result should be the same if the socket returns
+// ERR_CONNECTION_CLOSED. This is not expected to happen on an established
+// connection; a Read of size 0 is the expected behaviour. The key point of this
+// test is to confirm that ReadFrames() behaviour is identical in both cases.
+TEST_F(WebSocketBasicStreamSocketTest, SyncCloseWithErr) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED)};
+ CreateReadOnly(reads);
+
+ EXPECT_EQ(ERR_CONNECTION_CLOSED,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, AsyncCloseWithErr) {
+ MockRead reads[] = {MockRead(ASYNC, ERR_CONNECTION_CLOSED)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(ERR_CONNECTION_CLOSED, cb_.WaitForResult());
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, SyncErrorsPassedThrough) {
+ // ERR_INSUFFICIENT_RESOURCES here represents an arbitrary error that
+ // WebSocketBasicStream gives no special handling to.
+ MockRead reads[] = {MockRead(SYNCHRONOUS, ERR_INSUFFICIENT_RESOURCES)};
+ CreateReadOnly(reads);
+
+ EXPECT_EQ(ERR_INSUFFICIENT_RESOURCES,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, AsyncErrorsPassedThrough) {
+ MockRead reads[] = {MockRead(ASYNC, ERR_INSUFFICIENT_RESOURCES)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(ERR_INSUFFICIENT_RESOURCES, cb_.WaitForResult());
+}
+
+// If we get a frame followed by a close, we should receive them separately.
+TEST_F(WebSocketBasicStreamSocketTest, CloseAfterFrame) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, kSampleFrame, kSampleFrameSize),
+ MockRead(SYNCHRONOUS, "", 0)};
+ CreateReadOnly(reads);
+
+ EXPECT_EQ(OK, stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(1U, frame_chunks_.size());
+ frame_chunks_.clear();
+ EXPECT_EQ(ERR_CONNECTION_CLOSED,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+// Synchronous close after an async frame header is handled by a different code
+// path.
+TEST_F(WebSocketBasicStreamSocketTest, AsyncCloseAfterIncompleteHeader) {
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame, 1U),
+ MockRead(SYNCHRONOUS, "", 0)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(ERR_CONNECTION_CLOSED, cb_.WaitForResult());
+}
+
+// When Stream::Read returns ERR_CONNECTION_CLOSED we get the same result via a
+// slightly different code path.
+TEST_F(WebSocketBasicStreamSocketTest, AsyncErrCloseAfterIncompleteHeader) {
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame, 1U),
+ MockRead(SYNCHRONOUS, ERR_CONNECTION_CLOSED)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(ERR_CONNECTION_CLOSED, cb_.WaitForResult());
+}
+
+// If there was a frame read at the same time as the response headers (and the
+// handshake succeeded), then we should parse it.
+TEST_F(WebSocketBasicStreamSocketTest, HttpReadBufferIsUsed) {
+ SetHttpReadBuffer(kSampleFrame, kSampleFrameSize);
+ CreateNullStream();
+
+ EXPECT_EQ(OK, stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->data);
+ EXPECT_EQ(6, frame_chunks_[0]->data->size());
+}
+
+// Check that a frame whose header partially arrived at the end of the response
+// headers works correctly.
+TEST_F(WebSocketBasicStreamSocketTest, PartialFrameHeaderInHttpResponse) {
+ SetHttpReadBuffer(kSampleFrame, 1);
+ MockRead reads[] = {MockRead(ASYNC, kSampleFrame + 1, kSampleFrameSize - 1)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(OK, cb_.WaitForResult());
+ ASSERT_EQ(1U, frame_chunks_.size());
+ ASSERT_TRUE(frame_chunks_[0]->data);
+ EXPECT_EQ(6, frame_chunks_[0]->data->size());
+ ASSERT_TRUE(frame_chunks_[0]->header);
+ EXPECT_EQ(WebSocketFrameHeader::kOpCodeText,
+ frame_chunks_[0]->header->opcode);
+}
+
+// Check that an invalid frame results in an error.
+TEST_F(WebSocketBasicStreamSocketTest, SyncInvalidFrame) {
+ MockRead reads[] = {MockRead(SYNCHRONOUS, kInvalidFrame, kInvalidFrameSize)};
+ CreateReadOnly(reads);
+
+ EXPECT_EQ(ERR_WS_PROTOCOL_ERROR,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, AsyncInvalidFrame) {
+ MockRead reads[] = {MockRead(ASYNC, kInvalidFrame, kInvalidFrameSize)};
+ CreateReadOnly(reads);
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->ReadFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(ERR_WS_PROTOCOL_ERROR, cb_.WaitForResult());
+}
+
+// Check that writing a frame all at once works.
+TEST_F(WebSocketBasicStreamSocketTest, WriteAtOnce) {
+ MockWrite writes[] = {MockWrite(SYNCHRONOUS, kWriteFrame, kWriteFrameSize)};
+ CreateWriteOnly(writes);
+ frame_chunks_ = GenerateWriteFrame();
+
+ EXPECT_EQ(OK, stream_->WriteFrames(&frame_chunks_, cb_.callback()));
+}
+
+// Check that completely async writing works.
+TEST_F(WebSocketBasicStreamSocketTest, AsyncWriteAtOnce) {
+ MockWrite writes[] = {MockWrite(ASYNC, kWriteFrame, kWriteFrameSize)};
+ CreateWriteOnly(writes);
+ frame_chunks_ = GenerateWriteFrame();
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->WriteFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(OK, cb_.WaitForResult());
+}
+
+// Check that writing a frame to an extremely full kernel buffer (so that it
+// ends up being sent in bits) works. The WriteFrames() callback should not be
+// called until all parts have been written.
+TEST_F(WebSocketBasicStreamSocketTest, WriteInBits) {
+ MockWrite writes[] = {MockWrite(SYNCHRONOUS, kWriteFrame, 4),
+ MockWrite(ASYNC, kWriteFrame + 4, 4),
+ MockWrite(ASYNC, kWriteFrame + 8, kWriteFrameSize - 8)};
+ CreateWriteOnly(writes);
+ frame_chunks_ = GenerateWriteFrame();
+
+ ASSERT_EQ(ERR_IO_PENDING,
+ stream_->WriteFrames(&frame_chunks_, cb_.callback()));
+ EXPECT_EQ(OK, cb_.WaitForResult());
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, GetExtensionsWorks) {
+ extensions_ = "inflate-uuencode";
+ CreateNullStream();
+
+ EXPECT_EQ("inflate-uuencode", stream_->GetExtensions());
+}
+
+TEST_F(WebSocketBasicStreamSocketTest, GetSubProtocolWorks) {
+ sub_protocol_ = "cyberchat";
+ CreateNullStream();
+
+ EXPECT_EQ("cyberchat", stream_->GetSubProtocol());
+}
+
+} // namespace
+} // namespace net