diff options
author | ricea@chromium.org <ricea@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-17 13:42:54 +0000 |
---|---|---|
committer | ricea@chromium.org <ricea@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-17 13:42:54 +0000 |
commit | 999bcaadd3735a0003f2637ae15b763d44f3d867 (patch) | |
tree | 992111ca9fbd3ee2f004298e7da997859f68b2ba /net | |
parent | f6eed389769b4d0eb3ab9385e7b8500fbac5634a (diff) | |
download | chromium_src-999bcaadd3735a0003f2637ae15b763d44f3d867.zip chromium_src-999bcaadd3735a0003f2637ae15b763d44f3d867.tar.gz chromium_src-999bcaadd3735a0003f2637ae15b763d44f3d867.tar.bz2 |
WebSocketChannel implements the transport-independent parts of the WebSocket protocol. It also provides the interface to the content library.
The unit tests are still incomplete at the moment.
BUG=230756
TEST=net_unittests --gtest_filter='WebSocketChannel*'
Review URL: https://chromiumcodereview.appspot.com/12764006
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@212019 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r-- | net/net.gyp | 4 | ||||
-rw-r--r-- | net/websockets/README | 10 | ||||
-rw-r--r-- | net/websockets/websocket_channel.cc | 667 | ||||
-rw-r--r-- | net/websockets/websocket_channel.h | 259 | ||||
-rw-r--r-- | net/websockets/websocket_channel_test.cc | 1177 | ||||
-rw-r--r-- | net/websockets/websocket_event_interface.h | 76 | ||||
-rw-r--r-- | net/websockets/websocket_mux.h | 39 |
7 files changed, 2230 insertions, 2 deletions
diff --git a/net/net.gyp b/net/net.gyp index b97c69f..615e5be 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -1075,6 +1075,8 @@ 'url_request/url_request_throttler_manager.h', 'url_request/view_cache_helper.cc', 'url_request/view_cache_helper.h', + 'websockets/websocket_channel.cc', + 'websockets/websocket_channel.h', 'websockets/websocket_errors.cc', 'websockets/websocket_errors.h', 'websockets/websocket_frame.cc', @@ -1085,6 +1087,7 @@ 'websockets/websocket_handshake_handler.h', 'websockets/websocket_job.cc', 'websockets/websocket_job.h', + 'websockets/websocket_mux.h', 'websockets/websocket_net_log_params.cc', 'websockets/websocket_net_log_params.h', 'websockets/websocket_stream.cc', @@ -1829,6 +1832,7 @@ 'url_request/url_request_throttler_unittest.cc', 'url_request/url_request_unittest.cc', 'url_request/view_cache_helper_unittest.cc', + 'websockets/websocket_channel_test.cc', 'websockets/websocket_errors_unittest.cc', 'websockets/websocket_frame_parser_unittest.cc', 'websockets/websocket_frame_unittest.cc', diff --git a/net/websockets/README b/net/websockets/README index 5575201..558a451 100644 --- a/net/websockets/README +++ b/net/websockets/README @@ -17,8 +17,7 @@ websocket_handshake_handler_spdy2_unittest.cc websocket_handshake_handler_spdy3_unittest.cc websocket_job.cc websocket_job.h -websocket_job_spdy2_unittest.cc -websocket_job_spdy3_unittest.cc +websocket_job_unittest.cc websocket_net_log_params.cc websocket_net_log_params.h websocket_net_log_params_unittest.cc @@ -31,15 +30,22 @@ performs framing and implements protocol semantics in the browser process, and 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_channel.cc +websocket_channel.h +websocket_channel_test.cc websocket_errors.cc websocket_errors.h websocket_errors_unittest.cc +websocket_event_interface.h websocket_frame.cc websocket_frame.h websocket_frame_parser.cc websocket_frame_parser.h websocket_frame_parser_unittest.cc websocket_frame_unittest.cc +websocket_mux.h +websocket_stream_base.h +websocket_stream.cc websocket_stream.h A pre-submit check helps us keep this README file up-to-date: diff --git a/net/websockets/websocket_channel.cc b/net/websockets/websocket_channel.cc new file mode 100644 index 0000000..e27337c --- /dev/null +++ b/net/websockets/websocket_channel.cc @@ -0,0 +1,667 @@ +// 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_channel.h" + +#include <algorithm> + +#include "base/basictypes.h" // for size_t +#include "base/bind.h" +#include "base/safe_numerics.h" +#include "base/strings/string_util.h" +#include "net/base/big_endian.h" +#include "net/base/io_buffer.h" +#include "net/base/net_log.h" +#include "net/websockets/websocket_errors.h" +#include "net/websockets/websocket_event_interface.h" +#include "net/websockets/websocket_frame.h" +#include "net/websockets/websocket_mux.h" +#include "net/websockets/websocket_stream.h" + +namespace net { + +namespace { + +const int kDefaultSendQuotaLowWaterMark = 1 << 16; +const int kDefaultSendQuotaHighWaterMark = 1 << 17; +const size_t kWebSocketCloseCodeLength = 2; + +// Concatenate the data from two IOBufferWithSize objects into a single one. +IOBufferWithSize* ConcatenateIOBuffers( + const scoped_refptr<IOBufferWithSize>& part1, + const scoped_refptr<IOBufferWithSize>& part2) { + int newsize = part1->size() + part2->size(); + IOBufferWithSize* newbuffer = new IOBufferWithSize(newsize); + std::copy(part1->data(), part1->data() + part1->size(), newbuffer->data()); + std::copy(part2->data(), + part2->data() + part2->size(), + newbuffer->data() + part1->size()); + return newbuffer; +} + +} // namespace + +// A class to encapsulate a set of frames and information about the size of +// those frames. +class WebSocketChannel::SendBuffer { + public: + SendBuffer() : total_bytes_(0) {} + + // Add a WebSocketFrameChunk to the buffer and increase total_bytes_. + void AddFrame(scoped_ptr<WebSocketFrameChunk> chunk); + + // Return a pointer to the frames_ for write purposes. + ScopedVector<WebSocketFrameChunk>* GetFrames() { return &frames_; } + + private: + // The frames_ that will be sent in the next call to WriteFrames(). + ScopedVector<WebSocketFrameChunk> frames_; + + // The total size of the buffers in |frames_|. This will be used to measure + // the throughput of the link. + // TODO(ricea): Measure the throughput of the link. + size_t total_bytes_; +}; + +void WebSocketChannel::SendBuffer::AddFrame( + scoped_ptr<WebSocketFrameChunk> chunk) { + total_bytes_ += chunk->data->size(); + frames_.push_back(chunk.release()); +} + +// Implementation of WebSocketStream::ConnectDelegate that simply forwards the +// calls on to the WebSocketChannel that created it. +class WebSocketChannel::ConnectDelegate + : public WebSocketStream::ConnectDelegate { + public: + explicit ConnectDelegate(WebSocketChannel* creator) : creator_(creator) {} + + virtual void OnSuccess(scoped_ptr<WebSocketStream> stream) OVERRIDE { + creator_->OnConnectSuccess(stream.Pass()); + } + + virtual void OnFailure(uint16 websocket_error) OVERRIDE { + creator_->OnConnectFailure(websocket_error); + } + + private: + // A pointer to the WebSocketChannel that created us. We do not need to worry + // about this pointer being stale, because deleting WebSocketChannel cancels + // the connect process, deleting this object and preventing its callbacks from + // being called. + WebSocketChannel* const creator_; + + DISALLOW_COPY_AND_ASSIGN(ConnectDelegate); +}; + +WebSocketChannel::WebSocketChannel( + const GURL& socket_url, + scoped_ptr<WebSocketEventInterface> event_interface) + : socket_url_(socket_url), + event_interface_(event_interface.Pass()), + send_quota_low_water_mark_(kDefaultSendQuotaLowWaterMark), + send_quota_high_water_mark_(kDefaultSendQuotaHighWaterMark), + current_send_quota_(0), + closing_code_(0), + state_(FRESHLY_CONSTRUCTED) {} + +WebSocketChannel::~WebSocketChannel() { + // The stream may hold a pointer to read_frame_chunks_, and so it needs to be + // destroyed first. + stream_.reset(); +} + +void WebSocketChannel::SendAddChannelRequest( + const std::vector<std::string>& requested_subprotocols, + const GURL& origin, + URLRequestContext* url_request_context) { + // Delegate to the tested version. + SendAddChannelRequestWithFactory( + requested_subprotocols, + origin, + url_request_context, + base::Bind(&WebSocketStream::CreateAndConnectStream)); +} + +void WebSocketChannel::SendFrame(bool fin, + WebSocketFrameHeader::OpCode op_code, + const std::vector<char>& data) { + if (data.size() > INT_MAX) { + NOTREACHED() << "Frame size sanity check failed"; + return; + } + if (stream_ == NULL) { + LOG(DFATAL) << "Got SendFrame without a connection established; " + << "misbehaving renderer? fin=" << fin << " op_code=" << op_code + << " data.size()=" << data.size(); + return; + } + if (state_ == SEND_CLOSED || state_ == CLOSED) { + VLOG(1) << "SendFrame called in state " << state_ + << ". This may be a bug, or a harmless race."; + return; + } + if (state_ != CONNECTED) { + NOTREACHED() << "SendFrame() called in state " << state_; + return; + } + if (data.size() > base::checked_numeric_cast<size_t>(current_send_quota_)) { + FailChannel(SEND_GOING_AWAY, + kWebSocketMuxErrorSendQuotaViolation, + "Send quota exceeded"); + return; + } + if (!WebSocketFrameHeader::IsKnownDataOpCode(op_code)) { + LOG(DFATAL) << "Got SendFrame with bogus op_code " << op_code + << "; misbehaving renderer? fin=" << fin + << " data.size()=" << data.size(); + return; + } + current_send_quota_ -= data.size(); + // TODO(ricea): If current_send_quota_ has dropped below + // send_quota_low_water_mark_, we may want to consider increasing the "low + // water mark" and "high water mark", but only if we think we are not + // saturating the link to the WebSocket server. + // TODO(ricea): For kOpCodeText, do UTF-8 validation? + scoped_refptr<IOBufferWithSize> buffer(new IOBufferWithSize(data.size())); + std::copy(data.begin(), data.end(), buffer->data()); + SendIOBufferWithSize(fin, op_code, buffer); +} + +void WebSocketChannel::SendFlowControl(int64 quota) { + DCHECK_EQ(CONNECTED, state_); + // TODO(ricea): Add interface to WebSocketStream and implement. + // stream_->SendFlowControl(quota); +} + +void WebSocketChannel::StartClosingHandshake(uint16 code, + const std::string& reason) { + if (state_ == SEND_CLOSED || state_ == CLOSED) { + VLOG(1) << "StartClosingHandshake called in state " << state_ + << ". This may be a bug, or a harmless race."; + return; + } + if (state_ != CONNECTED) { + NOTREACHED() << "StartClosingHandshake() called in state " << state_; + return; + } + // TODO(ricea): Validate |code|? Check that |reason| is valid UTF-8? + // TODO(ricea): There should be a timeout for the closing handshake. + SendClose(code, reason); // Sets state_ to SEND_CLOSED +} + +void WebSocketChannel::SendAddChannelRequestForTesting( + const std::vector<std::string>& requested_subprotocols, + const GURL& origin, + URLRequestContext* url_request_context, + const WebSocketStreamFactory& factory) { + SendAddChannelRequestWithFactory( + requested_subprotocols, origin, url_request_context, factory); +} + +void WebSocketChannel::SendAddChannelRequestWithFactory( + const std::vector<std::string>& requested_subprotocols, + const GURL& origin, + URLRequestContext* url_request_context, + const WebSocketStreamFactory& factory) { + DCHECK_EQ(FRESHLY_CONSTRUCTED, state_); + scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate( + new ConnectDelegate(this)); + stream_request_ = factory.Run(socket_url_, + requested_subprotocols, + origin, + url_request_context, + BoundNetLog(), + connect_delegate.Pass()); + state_ = CONNECTING; +} + +void WebSocketChannel::OnConnectSuccess(scoped_ptr<WebSocketStream> stream) { + DCHECK(stream); + DCHECK_EQ(CONNECTING, state_); + stream_ = stream.Pass(); + state_ = CONNECTED; + event_interface_->OnAddChannelResponse(false, stream_->GetSubProtocol()); + + // TODO(ricea): Get flow control information from the WebSocketStream once we + // have a multiplexing WebSocketStream. + current_send_quota_ = send_quota_high_water_mark_; + event_interface_->OnFlowControl(send_quota_high_water_mark_); + + // We don't need this any more. + stream_request_.reset(); + ReadFrames(); +} + +void WebSocketChannel::OnConnectFailure(uint16 websocket_error) { + DCHECK_EQ(CONNECTING, state_); + state_ = CLOSED; + stream_request_.reset(); + event_interface_->OnAddChannelResponse(true, ""); +} + +void WebSocketChannel::WriteFrames() { + int result = OK; + do { + // This use of base::Unretained is safe because we own the WebSocketStream + // and destroying it cancels all callbacks. + result = stream_->WriteFrames( + data_being_sent_->GetFrames(), + base::Bind( + &WebSocketChannel::OnWriteDone, base::Unretained(this), false)); + if (result != ERR_IO_PENDING) { + OnWriteDone(true, result); + } + } while (result == OK && data_being_sent_); +} + +void WebSocketChannel::OnWriteDone(bool synchronous, int result) { + DCHECK_NE(FRESHLY_CONSTRUCTED, state_); + DCHECK_NE(CONNECTING, state_); + DCHECK_NE(ERR_IO_PENDING, result); + DCHECK(data_being_sent_); + switch (result) { + case OK: + if (data_to_send_next_) { + data_being_sent_ = data_to_send_next_.Pass(); + if (!synchronous) { + WriteFrames(); + } + } else { + data_being_sent_.reset(); + if (current_send_quota_ < send_quota_low_water_mark_) { + // TODO(ricea): Increase low_water_mark and high_water_mark if + // throughput is high, reduce them if throughput is low. Low water + // mark needs to be >= the bandwidth delay product *of the IPC + // channel*. Because factors like context-switch time, thread wake-up + // time, and bus speed come into play it is complex and probably needs + // to be determined empirically. + DCHECK_LE(send_quota_low_water_mark_, send_quota_high_water_mark_); + // TODO(ricea): Truncate quota by the quota specified by the remote + // server, if the protocol in use supports quota. + int fresh_quota = send_quota_high_water_mark_ - current_send_quota_; + current_send_quota_ += fresh_quota; + event_interface_->OnFlowControl(fresh_quota); + } + } + return; + + // If a recoverable error condition existed, it would go here. + + default: + DCHECK_LT(result, 0) + << "WriteFrames() should only return OK or ERR_ codes"; + stream_->Close(); + state_ = CLOSED; + event_interface_->OnDropChannel(kWebSocketErrorAbnormalClosure, + "Abnormal Closure"); + return; + } +} + +void WebSocketChannel::ReadFrames() { + int result = OK; + do { + // This use of base::Unretained is safe because we own the WebSocketStream, + // and any pending reads will be cancelled when it is destroyed. + result = stream_->ReadFrames( + &read_frame_chunks_, + base::Bind( + &WebSocketChannel::OnReadDone, base::Unretained(this), false)); + if (result != ERR_IO_PENDING) { + OnReadDone(true, result); + } + } while (result == OK); +} + +void WebSocketChannel::OnReadDone(bool synchronous, int result) { + DCHECK_NE(FRESHLY_CONSTRUCTED, state_); + DCHECK_NE(CONNECTING, state_); + DCHECK_NE(ERR_IO_PENDING, result); + switch (result) { + case OK: + // ReadFrames() must use ERR_CONNECTION_CLOSED for a closed connection + // with no data read, not an empty response. + DCHECK(!read_frame_chunks_.empty()) + << "ReadFrames() returned OK, but nothing was read."; + for (size_t i = 0; i < read_frame_chunks_.size(); ++i) { + scoped_ptr<WebSocketFrameChunk> chunk(read_frame_chunks_[i]); + read_frame_chunks_[i] = NULL; + ProcessFrameChunk(chunk.Pass()); + } + read_frame_chunks_.clear(); + // We need to always keep a call to ReadFrames pending. + if (!synchronous) { + ReadFrames(); + } + return; + + default: { + DCHECK_LT(result, 0) + << "ReadFrames() should only return OK or ERR_ codes"; + stream_->Close(); + state_ = CLOSED; + uint16 code = kWebSocketErrorAbnormalClosure; + std::string reason = "Abnormal Closure"; + if (closing_code_ != 0) { + code = closing_code_; + reason = closing_reason_; + } + event_interface_->OnDropChannel(code, reason); + return; + } + } +} + +void WebSocketChannel::ProcessFrameChunk( + scoped_ptr<WebSocketFrameChunk> chunk) { + bool is_first_chunk = false; + if (chunk->header) { + DCHECK(current_frame_header_ == NULL) + << "Received the header for a new frame without notification that " + << "the previous frame was complete."; + is_first_chunk = true; + current_frame_header_.swap(chunk->header); + if (current_frame_header_->masked) { + // RFC6455 Section 5.1 "A client MUST close a connection if it detects a + // masked frame." + FailChannel(SEND_REAL_ERROR, + kWebSocketErrorProtocolError, + "Masked frame from server"); + return; + } + } + if (!current_frame_header_) { + // If we rejected the previous chunk as invalid, then we will have reset + // current_frame_header_ to avoid using it. More chunks of the invalid frame + // may still arrive, so this is not necessarily a bug on our side. However, + // if this happens when state_ is CONNECTED, it is definitely a bug. + DCHECK(state_ != CONNECTED) << "Unexpected header-less frame received " + << "(final_chunk = " << chunk->final_chunk + << ", data size = " << chunk->data->size() + << ")"; + return; + } + scoped_refptr<IOBufferWithSize> data_buffer; + data_buffer.swap(chunk->data); + const bool is_final_chunk = chunk->final_chunk; + chunk.reset(); + WebSocketFrameHeader::OpCode opcode = current_frame_header_->opcode; + if (WebSocketFrameHeader::IsKnownControlOpCode(opcode)) { + if (!is_final_chunk) { + // TODO(ricea): Enforce a maximum size of 125 bytes on the control frames + // we accept. + VLOG(2) << "Encountered a split control frame, opcode " << opcode; + if (incomplete_control_frame_body_) { + // The really horrid case. We need to create a new IOBufferWithSize + // combining the new one and the old one. This should virtually never + // happen. + // TODO(ricea): This algorithm is O(N^2). Use a fixed 127-byte buffer + // instead. + VLOG(3) << "Hit the really horrid case"; + incomplete_control_frame_body_ = + ConcatenateIOBuffers(incomplete_control_frame_body_, data_buffer); + } else { + // The merely horrid case. Store the IOBufferWithSize to use when the + // rest of the control frame arrives. + incomplete_control_frame_body_.swap(data_buffer); + } + return; // Handle when complete. + } + if (incomplete_control_frame_body_) { + VLOG(2) << "Rejoining a split control frame, opcode " << opcode; + data_buffer = + ConcatenateIOBuffers(incomplete_control_frame_body_, data_buffer); + incomplete_control_frame_body_ = NULL; // Frame now complete. + } + } + + // Apply basic sanity checks to the |payload_length| field from the frame + // header. We can only apply a strict check when we know we have the whole + // frame in one chunk. + DCHECK_GE(current_frame_header_->payload_length, + base::checked_numeric_cast<uint64>(data_buffer->size())); + DCHECK(!is_first_chunk || !is_final_chunk || + current_frame_header_->payload_length == + base::checked_numeric_cast<uint64>(data_buffer->size())); + + // Respond to the frame appropriately to its type. + HandleFrame(opcode, is_first_chunk, is_final_chunk, data_buffer); + + if (is_final_chunk) { + // Make sure we do not apply this frame header to any future chunks. + current_frame_header_.reset(); + } +} + +void WebSocketChannel::HandleFrame( + const WebSocketFrameHeader::OpCode opcode, + bool is_first_chunk, + bool is_final_chunk, + const scoped_refptr<IOBufferWithSize>& data_buffer) { + DCHECK_NE(RECV_CLOSED, state_) + << "HandleFrame() does not support being called re-entrantly from within " + "SendClose()"; + if (state_ == CLOSED) { + std::string frame_name; + switch (opcode) { + case WebSocketFrameHeader::kOpCodeText: // fall-thru + case WebSocketFrameHeader::kOpCodeBinary: // fall-thru + case WebSocketFrameHeader::kOpCodeContinuation: + frame_name = "Data frame"; + break; + + case WebSocketFrameHeader::kOpCodePing: + frame_name = "Ping"; + break; + + case WebSocketFrameHeader::kOpCodePong: + frame_name = "Pong"; + break; + + case WebSocketFrameHeader::kOpCodeClose: + frame_name = "Close"; + break; + + default: + frame_name = "Unknown frame type"; + break; + } + // SEND_REAL_ERROR makes no difference here, as we won't send another Close + // frame. + FailChannel(SEND_REAL_ERROR, + kWebSocketErrorProtocolError, + frame_name + " received after close"); + return; + } + switch (opcode) { + case WebSocketFrameHeader::kOpCodeText: // fall-thru + case WebSocketFrameHeader::kOpCodeBinary: // fall-thru + case WebSocketFrameHeader::kOpCodeContinuation: + if (state_ == CONNECTED) { + const bool final = is_final_chunk && current_frame_header_->final; + // TODO(ricea): Can this copy be eliminated? + const char* const data_begin = data_buffer->data(); + const char* const data_end = data_begin + data_buffer->size(); + const std::vector<char> data(data_begin, data_end); + // TODO(ricea): Handle the (improbable) case when ReadFrames returns far + // more data at once than we want to send in a single IPC (in which case + // we need to buffer the data and return to the event loop with a + // callback to send the rest in 32K chunks). + + // Send the received frame to the renderer process. + event_interface_->OnDataFrame( + final, + is_first_chunk ? opcode : WebSocketFrameHeader::kOpCodeContinuation, + data); + } else { + VLOG(3) << "Ignored data packet received in state " << state_; + } + return; + + case WebSocketFrameHeader::kOpCodePing: + VLOG(1) << "Got Ping of size " << data_buffer->size(); + if (state_ == CONNECTED) { + SendIOBufferWithSize( + true, WebSocketFrameHeader::kOpCodePong, data_buffer); + } else { + VLOG(3) << "Ignored ping in state " << state_; + } + return; + + case WebSocketFrameHeader::kOpCodePong: + VLOG(1) << "Got Pong of size " << data_buffer->size(); + // We do not need to do anything with pong messages. + return; + + case WebSocketFrameHeader::kOpCodeClose: { + uint16 code = kWebSocketNormalClosure; + std::string reason; + ParseClose(data_buffer, &code, &reason); + // TODO(ricea): Find a way to safely log the message from the close + // message (escape control codes and so on). + VLOG(1) << "Got Close with code " << code; + switch (state_) { + case CONNECTED: + state_ = RECV_CLOSED; + SendClose(code, reason); // Sets state_ to CLOSED + event_interface_->OnClosingHandshake(); + closing_code_ = code; + closing_reason_ = reason; + break; + + case SEND_CLOSED: + state_ = CLOSED; + // From RFC6455 section 7.1.5: "Each endpoint + // will see the status code sent by the other end as _The WebSocket + // Connection Close Code_." + closing_code_ = code; + closing_reason_ = reason; + break; + + default: + LOG(DFATAL) << "Got Close in unexpected state " << state_; + break; + } + return; + } + + default: + FailChannel( + SEND_REAL_ERROR, kWebSocketErrorProtocolError, "Unknown opcode"); + return; + } +} + +void WebSocketChannel::SendIOBufferWithSize( + bool fin, + WebSocketFrameHeader::OpCode op_code, + const scoped_refptr<IOBufferWithSize>& buffer) { + DCHECK(state_ == CONNECTED || state_ == RECV_CLOSED); + DCHECK(stream_); + scoped_ptr<WebSocketFrameHeader> header(new WebSocketFrameHeader(op_code)); + header->final = fin; + header->masked = true; + header->payload_length = buffer->size(); + scoped_ptr<WebSocketFrameChunk> chunk(new WebSocketFrameChunk()); + chunk->header = header.Pass(); + chunk->final_chunk = true; + chunk->data = buffer; + if (data_being_sent_) { + // Either the link to the WebSocket server is saturated, or we are simply + // processing a batch of messages. + // TODO(ricea): We need to keep some statistics to work out which situation + // we are in and adjust quota appropriately. + if (!data_to_send_next_) + data_to_send_next_.reset(new SendBuffer); + data_to_send_next_->AddFrame(chunk.Pass()); + } else { + data_being_sent_.reset(new SendBuffer); + data_being_sent_->AddFrame(chunk.Pass()); + WriteFrames(); + } +} + +void WebSocketChannel::FailChannel(ExposeError expose, + uint16 code, + const std::string& reason) { + DCHECK_NE(FRESHLY_CONSTRUCTED, state_); + DCHECK_NE(CONNECTING, state_); + // TODO(ricea): Logging. + State old_state = state_; + if (state_ == CONNECTED) { + uint16 send_code = kWebSocketErrorGoingAway; + std::string send_reason = "Internal Error"; + if (expose == SEND_REAL_ERROR) { + send_code = code; + send_reason = reason; + } + SendClose(send_code, send_reason); // Sets state_ to SEND_CLOSED + } + // Careful study of RFC6455 section 7.1.7 and 7.1.1 indicates we should close + // the connection ourselves without waiting for the closing handshake. + stream_->Close(); + state_ = CLOSED; + + // We may be in the middle of processing several chunks. We should not re-use + // the frame header. + current_frame_header_.reset(); + if (old_state != CLOSED) { + event_interface_->OnDropChannel(code, reason); + } +} + +void WebSocketChannel::SendClose(uint16 code, const std::string& reason) { + DCHECK(state_ == CONNECTED || state_ == RECV_CLOSED); + // TODO(ricea): Ensure reason.length() <= 123 + size_t payload_length = kWebSocketCloseCodeLength + reason.length(); + scoped_refptr<IOBufferWithSize> body = + new IOBufferWithSize(base::checked_numeric_cast<int>(payload_length)); + WriteBigEndian(body->data(), code); + COMPILE_ASSERT(sizeof(code) == kWebSocketCloseCodeLength, + they_should_both_be_two); + std::copy( + reason.begin(), reason.end(), body->data() + kWebSocketCloseCodeLength); + SendIOBufferWithSize(true, WebSocketFrameHeader::kOpCodeClose, body); + state_ = (state_ == CONNECTED) ? SEND_CLOSED : CLOSED; +} + +void WebSocketChannel::ParseClose(const scoped_refptr<IOBufferWithSize>& buffer, + uint16* code, + std::string* reason) { + const char* data = buffer->data(); + size_t size = base::checked_numeric_cast<size_t>(buffer->size()); + reason->clear(); + if (size < kWebSocketCloseCodeLength) { + *code = kWebSocketErrorNoStatusReceived; + if (size != 0) { + VLOG(1) << "Close frame with payload size " << size << " received " + << "(the first byte is " << std::hex << static_cast<int>(data[0]) + << ")"; + return; + } + return; + } + uint16 unchecked_code = 0; + ReadBigEndian(data, &unchecked_code); + COMPILE_ASSERT(sizeof(unchecked_code) == kWebSocketCloseCodeLength, + they_should_both_be_two_bytes); + if (unchecked_code >= static_cast<uint16>(kWebSocketNormalClosure) && + unchecked_code <= + static_cast<uint16>(kWebSocketErrorPrivateReservedMax)) { + *code = unchecked_code; + } else { + VLOG(1) << "Close frame contained code outside of the valid range: " + << unchecked_code; + *code = kWebSocketErrorAbnormalClosure; + } + std::string text(data + kWebSocketCloseCodeLength, data + size); + // TODO(ricea): Is this check strict enough? In particular, check the + // "Security Considerations" from RFC3629. + if (IsStringUTF8(text)) { + reason->swap(text); + } +} + +} // namespace net diff --git a/net/websockets/websocket_channel.h b/net/websockets/websocket_channel.h new file mode 100644 index 0000000..7d34dae --- /dev/null +++ b/net/websockets/websocket_channel.h @@ -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. + +#ifndef NET_WEBSOCKETS_WEBSOCKET_CHANNEL_H_ +#define NET_WEBSOCKETS_WEBSOCKET_CHANNEL_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_export.h" +#include "net/websockets/websocket_frame.h" +#include "net/websockets/websocket_stream.h" + +namespace net { + +class URLRequestContext; +class WebSocketEventInterface; + +// Transport-independent implementation of WebSockets. Implements protocol +// semantics that do not depend on the underlying transport. Provides the +// interface to the content layer. Some WebSocket concepts are used here without +// definition; please see the RFC at http://tools.ietf.org/html/rfc6455 for +// clarification. +class NET_EXPORT WebSocketChannel { + public: + // The type of a WebSocketStream factory callback. Must match the signature of + // WebSocketStream::CreateAndConnectStream(). + typedef base::Callback<scoped_ptr<WebSocketStreamRequest>( + const GURL&, + const std::vector<std::string>&, + const GURL&, + URLRequestContext*, + const BoundNetLog&, + scoped_ptr<WebSocketStream::ConnectDelegate>)> WebSocketStreamFactory; + + // Creates a new WebSocketChannel with the specified parameters. + // SendAddChannelRequest() must be called immediately afterwards to start the + // connection process. + WebSocketChannel(const GURL& socket_url, + scoped_ptr<WebSocketEventInterface> event_interface); + virtual ~WebSocketChannel(); + + // Starts the connection process. + void SendAddChannelRequest( + const std::vector<std::string>& requested_protocols, + const GURL& origin, + URLRequestContext* url_request_context); + + // Sends a data frame to the remote side. The frame should usually be no + // larger than 32KB to prevent the time required to copy the buffers from from + // unduly delaying other tasks that need to run on the IO thread. This method + // has a hard limit of 2GB. It is the responsibility of the caller to ensure + // that they have sufficient send quota to send this data, otherwise the + // connection will be closed without sending. |fin| indicates the last frame + // in a message, equivalent to "FIN" as specified in section 5.2 of + // RFC6455. |data| is the "Payload Data". If |op_code| is kOpCodeText, or it + // is kOpCodeContinuation and the type the message is Text, then |data| must + // be a chunk of a valid UTF-8 message, however there is no requirement for + // |data| to be split on character boundaries. + void SendFrame(bool fin, + WebSocketFrameHeader::OpCode op_code, + const std::vector<char>& data); + + // Sends |quota| units of flow control to the remote side. If the underlying + // transport has a concept of |quota|, then it permits the remote server to + // send up to |quota| units of data. + void SendFlowControl(int64 quota); + + // Start the closing handshake for a client-initiated shutdown of the + // connection. There is no API to close the connection without a closing + // handshake, but destroying the WebSocketChannel object while connected will + // effectively do that. |code| must be in the range 1000-4999. |reason| should + // be a valid UTF-8 string or empty. + // + // This does *not* trigger the event OnClosingHandshake(). The caller should + // assume that the closing handshake has started and perform the equivalent + // processing to OnClosingHandshake() if necessary. + void StartClosingHandshake(uint16 code, const std::string& reason); + + // Starts the connection process, using a specified factory function rather + // than the default. This is exposed for testing. + void SendAddChannelRequestForTesting( + const std::vector<std::string>& requested_protocols, + const GURL& origin, + URLRequestContext* url_request_context, + const WebSocketStreamFactory& factory); + + private: + // We have a simple linear progression of states from FRESHLY_CONSTRUCTED to + // CLOSED, except that the SEND_CLOSED and RECV_CLOSED states may be skipped + // in case of error. + enum State { + FRESHLY_CONSTRUCTED, + CONNECTING, + CONNECTED, + SEND_CLOSED, // We have sent a Close frame but not received a Close frame. + RECV_CLOSED, // Used briefly between receiving a Close frame and sending + // the response. Once we have responded, the state changes + // to CLOSED. + CLOSED, // The Closing Handshake has completed or the connection is failed. + }; + + // When failing a channel, we may or may not want to send the real reason for + // failing to the remote server. This enum is used by FailChannel() to + // choose. + enum ExposeError { + SEND_REAL_ERROR, + SEND_GOING_AWAY, + }; + + // Our implementation of WebSocketStream::ConnectDelegate. We do not inherit + // from WebSocketStream::ConnectDelegate directly to avoid cluttering our + // public interface with the implementation of those methods, and because the + // lifetime of a WebSocketChannel is longer than the lifetime of the + // connection process. + class ConnectDelegate; + + // Starts the connection progress, using a specified factory function. + void SendAddChannelRequestWithFactory( + const std::vector<std::string>& requested_protocols, + const GURL& origin, + URLRequestContext* url_request_context, + const WebSocketStreamFactory& factory); + + // Success callback from WebSocketStream::CreateAndConnectStream(). Reports + // success to the event interface. + void OnConnectSuccess(scoped_ptr<WebSocketStream> stream); + + // Failure callback from WebSocketStream::CreateAndConnectStream(). Reports + // failure to the event interface. + void OnConnectFailure(uint16 websocket_error); + + // Calls WebSocketStream::WriteFrames() with the appropriate arguments + void WriteFrames(); + + // Callback from WebSocketStream::WriteFrames. Sends pending data or adjusts + // the send quota of the renderer channel as appropriate. |result| is a net + // error code, usually OK. If |synchronous| is true, then OnWriteDone() is + // being called from within the WriteFrames() loop and does not need to call + // WriteFrames() itself. + void OnWriteDone(bool synchronous, int result); + + // Calls WebSocketStream::ReadFrames() with the appropriate arguments. + void ReadFrames(); + + // Callback from WebSocketStream::ReadFrames. Handles any errors and processes + // the returned chunks appropriately to their type. |result| is a net error + // code. If |synchronous| is true, then OnReadDone() is being called from + // within the ReadFrames() loop and does not need to call ReadFrames() itself. + void OnReadDone(bool synchronous, int result); + + // Processes a single chunk that has been read from the stream. + void ProcessFrameChunk(scoped_ptr<WebSocketFrameChunk> chunk); + + // Handle a frame that we have received enough of to process. May call + // event_interface_ methods, send responses to the server, and change the + // value of state_. + void HandleFrame(const WebSocketFrameHeader::OpCode opcode, + bool is_first_chunk, + bool is_final_chunk, + const scoped_refptr<IOBufferWithSize>& data_buffer); + + // Low-level method to send a single frame. Used for both data and control + // frames. Either sends the frame immediately or buffers it to be scheduled + // when the current write finishes. |fin| and |op_code| are defined as for + // SendFrame() above, except that |op_code| may also be a control frame + // opcode. + void SendIOBufferWithSize(bool fin, + WebSocketFrameHeader::OpCode op_code, + const scoped_refptr<IOBufferWithSize>& buffer); + + // Perform the "Fail the WebSocket Connection" operation as defined in + // RFC6455. The supplied code and reason are sent back to the renderer in an + // OnDropChannel message. If state_ is CONNECTED then a Close message is sent + // to the remote host. If |expose| is SEND_REAL_ERROR then the remote host is + // given the same status code we gave the renderer; otherwise it is sent a + // fixed "Going Away" code. Resets current_frame_header_, closes the + // stream_, and sets state_ to CLOSED. + void FailChannel(ExposeError expose, uint16 code, const std::string& reason); + + // Sends a Close frame to Start the WebSocket Closing Handshake, or to respond + // to a Close frame from the server. + void SendClose(uint16 code, const std::string& reason); + + // Parses a Close frame. If no status code is supplied, then |code| is set to + // 1005 (No status code) with empty |reason|. If the supplied code is + // outside the valid range, then 1002 (Protocol error) is set instead. If the + // reason text is not valid UTF-8, then |reason| is set to an empty string + // instead. + void ParseClose(const scoped_refptr<IOBufferWithSize>& buffer, + uint16* code, + std::string* reason); + + // The URL to which we connect. + const GURL socket_url_; + + // The object receiving events. + const scoped_ptr<WebSocketEventInterface> event_interface_; + + // The WebSocketStream to which we are sending/receiving data. + scoped_ptr<WebSocketStream> stream_; + + // A data structure containing a vector of frames to be sent and the total + // number of bytes contained in the vector. + class SendBuffer; + // Data that is currently pending write, or NULL if no write is pending. + scoped_ptr<SendBuffer> data_being_sent_; + // Data that is queued up to write after the current write completes. + // Only non-NULL when such data actually exists. + scoped_ptr<SendBuffer> data_to_send_next_; + + // Destination for the current call to WebSocketStream::ReadFrames + ScopedVector<WebSocketFrameChunk> read_frame_chunks_; + // Frame header for the frame currently being received. Only non-NULL while we + // are processing the frame. If the frame arrives in multiple chunks, can + // remain non-NULL while we wait for additional chunks to arrive. If the + // header of the frame was invalid, this is set to NULL, the channel is + // failed, and subsequent chunks of the same frame will be ignored. + scoped_ptr<WebSocketFrameHeader> current_frame_header_; + // Handle to an in-progress WebSocketStream creation request. Only non-NULL + // during the connection process. + scoped_ptr<WebSocketStreamRequest> stream_request_; + // Although it will almost never happen in practice, we can be passed an + // incomplete control frame, in which case we need to keep the data around + // long enough to reassemble it. This variable will be NULL the rest of the + // time. + scoped_refptr<IOBufferWithSize> incomplete_control_frame_body_; + // The point at which we give the renderer a quota refresh (quota units). + // "quota units" are currently bytes. TODO(ricea): Update the definition of + // quota units when necessary. + int send_quota_low_water_mark_; + // The amount which we refresh the quota to when it reaches the + // low_water_mark (quota units). + int send_quota_high_water_mark_; + // The current amount of quota that the renderer has available for sending + // on this logical channel (quota units). + int current_send_quota_; + + // Storage for the status code and reason from the time we receive the Close + // frame until the connection is closed and we can call OnDropChannel(). + uint16 closing_code_; + std::string closing_reason_; + + // The current state of the channel. Mainly used for sanity checking, but also + // used to track the close state. + State state_; + + DISALLOW_COPY_AND_ASSIGN(WebSocketChannel); +}; + +} // namespace net + +#endif // NET_WEBSOCKETS_WEBSOCKET_CHANNEL_H_ diff --git a/net/websockets/websocket_channel_test.cc b/net/websockets/websocket_channel_test.cc new file mode 100644 index 0000000..25e20ef --- /dev/null +++ b/net/websockets/websocket_channel_test.cc @@ -0,0 +1,1177 @@ +// 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_channel.h" + +#include <iostream> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_piece.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request_context.h" +#include "net/websockets/websocket_errors.h" +#include "net/websockets/websocket_event_interface.h" +#include "net/websockets/websocket_mux.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +// Printing helpers to allow GoogleMock to print frame chunks. These are +// explicitly designed to look like the static initialisation format we use in +// these tests. They have to live in the net namespace in order to be found by +// GoogleMock; a nested anonymous namespace will not work. + +std::ostream& operator<<(std::ostream& os, const WebSocketFrameHeader& header) { + return os << "{" << (header.final ? "FINAL_FRAME" : "NOT_FINAL_FRAME") << ", " + << header.opcode << ", " + << (header.masked ? "MASKED" : "NOT_MASKED") << ", " + << header.payload_length << "}"; +} + +std::ostream& operator<<(std::ostream& os, const WebSocketFrameChunk& chunk) { + os << "{"; + if (chunk.header) { + os << *chunk.header; + } else { + os << "{NO_HEADER}"; + } + return os << ", " << (chunk.final_chunk ? "FINAL_CHUNK" : "NOT_FINAL_CHUNK") + << ", \"" << base::StringPiece(chunk.data->data(), + chunk.data->size()) << "\"}"; +} + +namespace { + +using ::testing::AnyNumber; +using ::testing::Field; +using ::testing::InSequence; +using ::testing::MockFunction; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::StrictMock; +using ::testing::_; + +// This mock is for testing expectations about how the EventInterface is used. +class MockWebSocketEventInterface : public WebSocketEventInterface { + public: + MOCK_METHOD2(OnAddChannelResponse, void(bool, const std::string&)); + MOCK_METHOD3(OnDataFrame, + void(bool, WebSocketMessageType, const std::vector<char>&)); + MOCK_METHOD1(OnFlowControl, void(int64)); + MOCK_METHOD0(OnClosingHandshake, void(void)); + MOCK_METHOD2(OnDropChannel, void(uint16, const std::string&)); +}; + +// This fake EventInterface is for tests which need a WebSocketEventInterface +// implementation but are not verifying how it is used. +class FakeWebSocketEventInterface : public WebSocketEventInterface { + virtual void OnAddChannelResponse( + bool fail, + const std::string& selected_protocol) OVERRIDE {} + virtual void OnDataFrame(bool fin, + WebSocketMessageType type, + const std::vector<char>& data) OVERRIDE {} + virtual void OnFlowControl(int64 quota) OVERRIDE {} + virtual void OnClosingHandshake() OVERRIDE {} + virtual void OnDropChannel(uint16 code, const std::string& reason) OVERRIDE {} +}; + +// This fake WebSocketStream is for tests that require a WebSocketStream but are +// not testing the way it is used. It has minimal functionality to return +// the |protocol| and |extensions| that it was constructed with. +class FakeWebSocketStream : public WebSocketStream { + public: + // Constructs with empty protocol and extensions. + FakeWebSocketStream() {} + + // Constructs with specified protocol and extensions. + FakeWebSocketStream(const std::string& protocol, + const std::string& extensions) + : protocol_(protocol), extensions_(extensions) {} + + virtual int SendHandshakeRequest( + const GURL& url, + const HttpRequestHeaders& headers, + HttpResponseInfo* response_info, + const CompletionCallback& callback) OVERRIDE { + return ERR_IO_PENDING; + } + + virtual int ReadHandshakeResponse( + const CompletionCallback& callback) OVERRIDE { + return ERR_IO_PENDING; + } + + virtual int ReadFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + return ERR_IO_PENDING; + } + + virtual int WriteFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + return ERR_IO_PENDING; + } + + virtual void Close() OVERRIDE {} + + // Returns the string passed to the constructor. + virtual std::string GetSubProtocol() const OVERRIDE { return protocol_; } + + // Returns the string passed to the constructor. + virtual std::string GetExtensions() const OVERRIDE { return extensions_; } + + private: + // The string to return from GetSubProtocol(). + std::string protocol_; + + // The string to return from GetExtensions(). + std::string extensions_; +}; + +// To make the static initialisers easier to read, we use enums rather than +// bools. + +// NO_HEADER means there shouldn't be a header included in the generated +// WebSocketFrameChunk. The static initialiser always has a header, but we can +// avoid specifying the rest of the fields. +enum IsFinal { + NO_HEADER, + NOT_FINAL_FRAME, + FINAL_FRAME +}; + +enum IsMasked { + NOT_MASKED, + MASKED +}; + +enum IsFinalChunk { + NOT_FINAL_CHUNK, + FINAL_CHUNK +}; + +// This is used to initialise a WebSocketFrameChunk but is statically +// initialisable. +struct InitFrameChunk { + struct FrameHeader { + IsFinal final; + // Reserved fields omitted for now. Add them if you need them. + WebSocketFrameHeader::OpCode opcode; + IsMasked masked; + // payload_length is the length of the whole frame. The length of the data + // members from every chunk in the frame must add up to the payload_length. + uint64 payload_length; + }; + FrameHeader header; + + // Directly equivalent to WebSocketFrameChunk::final_chunk + IsFinalChunk final_chunk; + + // Will be used to create the IOBuffer member. Can be NULL for NULL data. Is a + // nul-terminated string for ease-of-use. This means it is not 8-bit clean, + // but this is not an issue for test data. + const char* const data; +}; + +// Convert a const array of InitFrameChunks to the format used at +// runtime. Templated on the size of the array to save typing. +template <size_t N> +ScopedVector<WebSocketFrameChunk> CreateFrameChunkVector( + const InitFrameChunk (&source_chunks)[N]) { + ScopedVector<WebSocketFrameChunk> result_chunks; + result_chunks.reserve(N); + for (size_t i = 0; i < N; ++i) { + scoped_ptr<WebSocketFrameChunk> result_chunk(new WebSocketFrameChunk); + size_t chunk_length = + source_chunks[i].data ? strlen(source_chunks[i].data) : 0; + if (source_chunks[i].header.final != NO_HEADER) { + const InitFrameChunk::FrameHeader& source_header = + source_chunks[i].header; + scoped_ptr<WebSocketFrameHeader> result_header( + new WebSocketFrameHeader(source_header.opcode)); + result_header->final = (source_header.final == FINAL_FRAME); + result_header->opcode = source_header.opcode; + result_header->masked = (source_header.masked == MASKED); + result_header->payload_length = source_header.payload_length; + DCHECK(chunk_length <= source_header.payload_length); + result_chunk->header.swap(result_header); + } + result_chunk->final_chunk = (source_chunks[i].final_chunk == FINAL_CHUNK); + if (source_chunks[i].data) { + result_chunk->data = new IOBufferWithSize(chunk_length); + memcpy(result_chunk->data->data(), source_chunks[i].data, chunk_length); + } + result_chunks.push_back(result_chunk.release()); + } + return result_chunks.Pass(); +} + +// A GoogleMock action which can be used to respond to call to ReadFrames with +// some frames. Use like ReadFrames(_, _).WillOnce(ReturnChunks(chunks)); +ACTION_P(ReturnChunks, source_chunks) { + *arg0 = CreateFrameChunkVector(source_chunks); + return OK; +} + +// A FakeWebSocketStream whose ReadFrames() function returns data. +class ReadableFakeWebSocketStream : public FakeWebSocketStream { + public: + enum IsSync { + SYNC, + ASYNC + }; + + // After constructing the object, call PrepareReadFrames() once for each + // time you wish it to return from the test. + ReadableFakeWebSocketStream() : index_(0), read_frames_pending_(false) {} + + // Check that all the prepared responses have been consumed. + virtual ~ReadableFakeWebSocketStream() { + CHECK(index_ >= responses_.size()); + CHECK(!read_frames_pending_); + } + + // Prepares a fake responses. Fake responses will be returned from + // ReadFrames() in the same order they were prepared with PrepareReadFrames() + // and PrepareReadFramesError(). If |async| is ASYNC, then ReadFrames() will + // return ERR_IO_PENDING and the callback will be scheduled to run on the + // message loop. This requires the test case to run the message loop. If + // |async| is SYNC, the response will be returned synchronously. |error| is + // returned directly from ReadFrames() in the synchronous case, or passed to + // the callback in the asynchronous case. |chunks| will be converted to a + // ScopedVector<WebSocketFrameChunks> and copied to the pointer that was + // passed to ReadFrames(). + template <size_t N> + void PrepareReadFrames(IsSync async, + int error, + const InitFrameChunk (&chunks)[N]) { + responses_.push_back( + new Response(async, error, CreateFrameChunkVector(chunks))); + } + + // Prepares a fake error response (ie. there is no data). + void PrepareReadFramesError(IsSync async, int error) { + responses_.push_back( + new Response(async, error, ScopedVector<WebSocketFrameChunk>())); + } + + virtual int ReadFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + CHECK(!read_frames_pending_); + if (index_ >= responses_.size()) + return ERR_IO_PENDING; + if (responses_[index_]->async == ASYNC) { + read_frames_pending_ = true; + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&ReadableFakeWebSocketStream::DoCallback, + base::Unretained(this), + frame_chunks, + callback)); + return ERR_IO_PENDING; + } else { + frame_chunks->swap(responses_[index_]->chunks); + return responses_[index_++]->error; + } + } + + private: + void DoCallback(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) { + read_frames_pending_ = false; + frame_chunks->swap(responses_[index_]->chunks); + callback.Run(responses_[index_++]->error); + return; + } + + struct Response { + Response(IsSync async, int error, ScopedVector<WebSocketFrameChunk> chunks) + : async(async), error(error), chunks(chunks.Pass()) {} + + IsSync async; + int error; + ScopedVector<WebSocketFrameChunk> chunks; + + private: + // Bad things will happen if we attempt to copy or assign "chunks". + DISALLOW_COPY_AND_ASSIGN(Response); + }; + ScopedVector<Response> responses_; + + // The index into the responses_ array of the next response to be returned. + size_t index_; + + // True when an async response from ReadFrames() is pending. This only applies + // to "real" async responses. Once all the prepared responses have been + // returned, ReadFrames() returns ERR_IO_PENDING but read_frames_pending_ is + // not set to true. + bool read_frames_pending_; +}; + +// A FakeWebSocketStream where writes always complete successfully and +// synchronously. +class WriteableFakeWebSocketStream : public FakeWebSocketStream { + public: + virtual int WriteFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + return OK; + } +}; + +// A FakeWebSocketStream where writes always fail. +class UnWriteableFakeWebSocketStream : public FakeWebSocketStream { + public: + virtual int WriteFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + return ERR_CONNECTION_RESET; + } +}; + +// A FakeWebSocketStream which echoes any frames written back. Clears the +// "masked" header bit, but makes no other checks for validity. Tests using this +// must run the MessageLoop to receive the callback(s). If a message with opcode +// Close is echoed, then an ERR_CONNECTION_CLOSED is returned in the next +// callback. The test must do something to cause WriteFrames() to be called, +// otherwise the ReadFrames() callback will never be called. +class EchoeyFakeWebSocketStream : public FakeWebSocketStream { + public: + EchoeyFakeWebSocketStream() : read_frame_chunks_(NULL), done_(false) {} + + virtual int WriteFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + // Users of WebSocketStream will not expect the ReadFrames() callback to be + // called from within WriteFrames(), so post it to the message loop instead. + stored_frame_chunks_.insert( + stored_frame_chunks_.end(), frame_chunks->begin(), frame_chunks->end()); + frame_chunks->weak_clear(); + PostCallback(); + return OK; + } + + virtual int ReadFrames(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback) OVERRIDE { + read_callback_ = callback; + read_frame_chunks_ = frame_chunks; + if (done_) + PostCallback(); + return ERR_IO_PENDING; + } + + private: + void PostCallback() { + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&EchoeyFakeWebSocketStream::DoCallback, + base::Unretained(this))); + } + + void DoCallback() { + if (done_) { + read_callback_.Run(ERR_CONNECTION_CLOSED); + } else if (!stored_frame_chunks_.empty()) { + done_ = MoveFrameChunks(read_frame_chunks_); + read_frame_chunks_ = NULL; + read_callback_.Run(OK); + } + } + + // Copy the chunks stored in stored_frame_chunks_ to |out|, while clearing the + // "masked" header bit. Returns true if a Close Frame was seen, false + // otherwise. + bool MoveFrameChunks(ScopedVector<WebSocketFrameChunk>* out) { + bool seen_close = false; + *out = stored_frame_chunks_.Pass(); + for (ScopedVector<WebSocketFrameChunk>::iterator it = out->begin(); + it != out->end(); + ++it) { + WebSocketFrameHeader* header = (*it)->header.get(); + if (header) { + header->masked = false; + if (header->opcode == WebSocketFrameHeader::kOpCodeClose) + seen_close = true; + } + } + return seen_close; + } + + ScopedVector<WebSocketFrameChunk> stored_frame_chunks_; + CompletionCallback read_callback_; + // Owned by the caller of ReadFrames(). + ScopedVector<WebSocketFrameChunk>* read_frame_chunks_; + // True if we should close the connection. + bool done_; +}; + +// This mock is for verifying that WebSocket protocol semantics are obeyed (to +// the extent that they are implemented in WebSocketCommon). +class MockWebSocketStream : public WebSocketStream { + public: + MOCK_METHOD2(ReadFrames, + int(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback)); + MOCK_METHOD2(WriteFrames, + int(ScopedVector<WebSocketFrameChunk>* frame_chunks, + const CompletionCallback& callback)); + MOCK_METHOD0(Close, void()); + MOCK_CONST_METHOD0(GetSubProtocol, std::string()); + MOCK_CONST_METHOD0(GetExtensions, std::string()); + MOCK_METHOD0(AsWebSocketStream, WebSocketStream*()); + MOCK_METHOD4(SendHandshakeRequest, + int(const GURL& url, + const HttpRequestHeaders& headers, + HttpResponseInfo* response_info, + const CompletionCallback& callback)); + MOCK_METHOD1(ReadHandshakeResponse, int(const CompletionCallback& callback)); +}; + +struct ArgumentCopyingWebSocketFactory { + scoped_ptr<WebSocketStreamRequest> Factory( + const GURL& socket_url, + const std::vector<std::string>& requested_subprotocols, + const GURL& origin, + URLRequestContext* url_request_context, + const BoundNetLog& net_log, + scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate) { + this->socket_url = socket_url; + this->requested_subprotocols = requested_subprotocols; + this->origin = origin; + this->url_request_context = url_request_context; + this->net_log = net_log; + this->connect_delegate = connect_delegate.Pass(); + return make_scoped_ptr(new WebSocketStreamRequest); + } + + GURL socket_url; + GURL origin; + std::vector<std::string> requested_subprotocols; + URLRequestContext* url_request_context; + BoundNetLog net_log; + scoped_ptr<WebSocketStream::ConnectDelegate> connect_delegate; +}; + +// Converts a std::string to a std::vector<char>. For test purposes, it is +// convenient to be able to specify data as a string, but the +// WebSocketEventInterface requires the vector<char> type. +std::vector<char> AsVector(const std::string& s) { + return std::vector<char>(s.begin(), s.end()); +} + +// Base class for all test fixtures. +class WebSocketChannelTest : public ::testing::Test { + protected: + WebSocketChannelTest() : stream_(new FakeWebSocketStream) {} + + // Creates a new WebSocketChannel and connects it, using the settings stored + // in |connect_data_|. + void CreateChannelAndConnect() { + channel_.reset( + new WebSocketChannel(connect_data_.url, CreateEventInterface())); + channel_->SendAddChannelRequestForTesting( + connect_data_.requested_subprotocols, + connect_data_.origin, + &connect_data_.url_request_context, + base::Bind(&ArgumentCopyingWebSocketFactory::Factory, + base::Unretained(&connect_data_.factory))); + } + + // Same as CreateChannelAndConnect(), but calls the on_success callback as + // well. This method is virtual so that subclasses can also set the stream. + virtual void CreateChannelAndConnectSuccessfully() { + CreateChannelAndConnect(); + connect_data_.factory.connect_delegate->OnSuccess(stream_.Pass()); + } + + // Returns a WebSocketEventInterface to be passed to the WebSocketChannel. + // This implementation returns a newly-created fake. Subclasses may return a + // mock instead. + virtual scoped_ptr<WebSocketEventInterface> CreateEventInterface() { + return scoped_ptr<WebSocketEventInterface>(new FakeWebSocketEventInterface); + } + + // This method serves no other purpose than to provide a nice syntax for + // assigning to stream_. class T must be a subclass of WebSocketStream or you + // will have unpleasant compile errors. + template <class T> + void set_stream(scoped_ptr<T> stream) { + // Since the definition of "PassAs" depends on the type T, the C++ standard + // requires the "template" keyword to indicate that "PassAs" should be + // parsed as a template method. + stream_ = stream.template PassAs<WebSocketStream>(); + } + + // A struct containing the data that will be used to connect the channel. + struct ConnectData { + // URL to (pretend to) connect to. + GURL url; + // Origin of the request + GURL origin; + // Requested protocols for the request. + std::vector<std::string> requested_subprotocols; + // URLRequestContext object. + URLRequestContext url_request_context; + // A fake WebSocketFactory that just records its arguments. + ArgumentCopyingWebSocketFactory factory; + }; + ConnectData connect_data_; + + // The channel we are testing. Not initialised until SetChannel() is called. + scoped_ptr<WebSocketChannel> channel_; + + // A mock or fake stream for tests that need one. + scoped_ptr<WebSocketStream> stream_; +}; + +// Base class for tests which verify that EventInterface methods are called +// appropriately. +class WebSocketChannelEventInterfaceTest : public WebSocketChannelTest { + protected: + WebSocketChannelEventInterfaceTest() + : event_interface_(new StrictMock<MockWebSocketEventInterface>) {} + + // Tests using this fixture must set expectations on the event_interface_ mock + // object before calling CreateChannelAndConnect() or + // CreateChannelAndConnectSuccessfully(). This will only work once per test + // case, but once should be enough. + virtual scoped_ptr<WebSocketEventInterface> CreateEventInterface() OVERRIDE { + return scoped_ptr<WebSocketEventInterface>(event_interface_.release()); + } + + scoped_ptr<MockWebSocketEventInterface> event_interface_; +}; + +// Base class for tests which verify that WebSocketStream methods are called +// appropriately by using a MockWebSocketStream. +class WebSocketChannelStreamTest : public WebSocketChannelTest { + protected: + WebSocketChannelStreamTest() + : mock_stream_(new StrictMock<MockWebSocketStream>) {} + + virtual void CreateChannelAndConnectSuccessfully() OVERRIDE { + set_stream(mock_stream_.Pass()); + WebSocketChannelTest::CreateChannelAndConnectSuccessfully(); + } + + scoped_ptr<MockWebSocketStream> mock_stream_; +}; + +// Simple test that everything that should be passed to the factory function is +// passed to the factory function. +TEST_F(WebSocketChannelTest, EverythingIsPassedToTheFactoryFunction) { + connect_data_.url = GURL("ws://example.com/test"); + connect_data_.origin = GURL("http://example.com/test"); + connect_data_.requested_subprotocols.push_back("Sinbad"); + + CreateChannelAndConnect(); + + EXPECT_EQ(connect_data_.url, connect_data_.factory.socket_url); + EXPECT_EQ(connect_data_.origin, connect_data_.factory.origin); + EXPECT_EQ(connect_data_.requested_subprotocols, + connect_data_.factory.requested_subprotocols); + EXPECT_EQ(&connect_data_.url_request_context, + connect_data_.factory.url_request_context); +} + +TEST_F(WebSocketChannelEventInterfaceTest, ConnectSuccessReported) { + // false means success. + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, "")); + // OnFlowControl is always called immediately after connect to provide initial + // quota to the renderer. + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + + CreateChannelAndConnect(); + + connect_data_.factory.connect_delegate->OnSuccess(stream_.Pass()); +} + +TEST_F(WebSocketChannelEventInterfaceTest, ConnectFailureReported) { + // true means failure. + EXPECT_CALL(*event_interface_, OnAddChannelResponse(true, "")); + + CreateChannelAndConnect(); + + connect_data_.factory.connect_delegate + ->OnFailure(kWebSocketErrorNoStatusReceived); +} + +TEST_F(WebSocketChannelEventInterfaceTest, ProtocolPassed) { + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, "Bob")); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + + CreateChannelAndConnect(); + + connect_data_.factory.connect_delegate->OnSuccess( + scoped_ptr<WebSocketStream>(new FakeWebSocketStream("Bob", ""))); +} + +// The first frames from the server can arrive together with the handshake, in +// which case they will be available as soon as ReadFrames() is called the first +// time. +TEST_F(WebSocketChannelEventInterfaceTest, DataLeftFromHandshake) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 5}, + FINAL_CHUNK, "HELLO"}, + }; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::SYNC, OK, chunks); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + true, WebSocketFrameHeader::kOpCodeText, AsVector("HELLO"))); + } + + CreateChannelAndConnectSuccessfully(); +} + +// A remote server could accept the handshake, but then immediately send a +// Close frame. +TEST_F(WebSocketChannelEventInterfaceTest, CloseAfterHandshake) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeClose, NOT_MASKED, 23}, + FINAL_CHUNK, "\x03\xf3Internal Server Error"}, + }; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::SYNC, OK, chunks); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::SYNC, + ERR_CONNECTION_CLOSED); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, OnClosingHandshake()); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorInternalServerError, + "Internal Server Error")); + } + + CreateChannelAndConnectSuccessfully(); +} + +// A remote server could close the connection immediately after sending the +// handshake response (most likely a bug in the server). +TEST_F(WebSocketChannelEventInterfaceTest, ConnectionCloseAfterHandshake) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::SYNC, + ERR_CONNECTION_CLOSED); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorAbnormalClosure, _)); + } + + CreateChannelAndConnectSuccessfully(); +} + +TEST_F(WebSocketChannelEventInterfaceTest, NormalAsyncRead) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 5}, + FINAL_CHUNK, "HELLO"}, + }; + // We use this checkpoint object to verify that the callback isn't called + // until we expect it to be. + MockFunction<void(int)> checkpoint; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(1)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + true, WebSocketFrameHeader::kOpCodeText, AsVector("HELLO"))); + EXPECT_CALL(checkpoint, Call(2)); + } + + CreateChannelAndConnectSuccessfully(); + checkpoint.Call(1); + base::MessageLoop::current()->RunUntilIdle(); + checkpoint.Call(2); +} + +// Extra data can arrive while a read is being processed, resulting in the next +// read completing synchronously. +TEST_F(WebSocketChannelEventInterfaceTest, AsyncThenSyncRead) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks1[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 5}, + FINAL_CHUNK, "HELLO"}, + }; + static const InitFrameChunk chunks2[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 5}, + FINAL_CHUNK, "WORLD"}, + }; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks1); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::SYNC, OK, chunks2); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + true, WebSocketFrameHeader::kOpCodeText, AsVector("HELLO"))); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + true, WebSocketFrameHeader::kOpCodeText, AsVector("WORLD"))); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// Data frames that arrive in fragments are turned into individual frames +TEST_F(WebSocketChannelEventInterfaceTest, FragmentedFrames) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + // Here we have one message split into 3 frames which arrive in 3 chunks. The + // first frame is entirely in the first chunk, the second frame is split + // across all the chunks, and the final frame is entirely in the final + // chunk. The frame fragments are converted to separate frames so that they + // can be delivered immediatedly. So the EventInterface should see a Text + // message with 5 frames. + static const InitFrameChunk chunks1[] = { + {{NOT_FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 5}, + FINAL_CHUNK, "THREE"}, + {{NOT_FINAL_FRAME, WebSocketFrameHeader::kOpCodeContinuation, NOT_MASKED, + 7}, + NOT_FINAL_CHUNK, " "}, + }; + static const InitFrameChunk chunks2[] = {{{NO_HEADER}, NOT_FINAL_CHUNK, + "SMALL"}}; + static const InitFrameChunk chunks3[] = { + {{NO_HEADER}, FINAL_CHUNK, " "}, + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeContinuation, NOT_MASKED, 6}, + FINAL_CHUNK, "FRAMES"}, + }; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks1); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks2); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks3); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + false, WebSocketFrameHeader::kOpCodeText, AsVector("THREE"))); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + false, WebSocketFrameHeader::kOpCodeContinuation, AsVector(" "))); + EXPECT_CALL(*event_interface_, + OnDataFrame(false, + WebSocketFrameHeader::kOpCodeContinuation, + AsVector("SMALL"))); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + false, WebSocketFrameHeader::kOpCodeContinuation, AsVector(" "))); + EXPECT_CALL(*event_interface_, + OnDataFrame(true, + WebSocketFrameHeader::kOpCodeContinuation, + AsVector("FRAMES"))); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// In the case when a single-frame message because fragmented, it must be +// correctly transformed to multiple frames. +TEST_F(WebSocketChannelEventInterfaceTest, MessageFragmentation) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + // A single-frame Text message arrives in three chunks. This should be + // delivered as three frames. + static const InitFrameChunk chunks1[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 12}, + NOT_FINAL_CHUNK, "TIME"}, + }; + static const InitFrameChunk chunks2[] = {{{NO_HEADER}, NOT_FINAL_CHUNK, + " FOR "}}; + static const InitFrameChunk chunks3[] = {{{NO_HEADER}, FINAL_CHUNK, "TEA"}}; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks1); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks2); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks3); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + false, WebSocketFrameHeader::kOpCodeText, AsVector("TIME"))); + EXPECT_CALL(*event_interface_, + OnDataFrame(false, + WebSocketFrameHeader::kOpCodeContinuation, + AsVector(" FOR "))); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + true, WebSocketFrameHeader::kOpCodeContinuation, AsVector("TEA"))); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// If a control message is fragmented, it must be re-assembled before being +// delivered. A control message can only be fragmented at the network level; it +// is not permitted to be split into multiple frames. +TEST_F(WebSocketChannelEventInterfaceTest, FragmentedControlMessage) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks1[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeClose, NOT_MASKED, 7}, + NOT_FINAL_CHUNK, "\x03\xe8"}, + }; + static const InitFrameChunk chunks2[] = {{{NO_HEADER}, NOT_FINAL_CHUNK, + "Clo"}}; + static const InitFrameChunk chunks3[] = {{{NO_HEADER}, FINAL_CHUNK, "se"}}; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks1); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks2); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks3); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::ASYNC, + ERR_CONNECTION_CLOSED); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, OnClosingHandshake()); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketNormalClosure, "Close")); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// Connection closed by the remote host without a closing handshake. +TEST_F(WebSocketChannelEventInterfaceTest, AsyncAbnormalClosure) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::ASYNC, + ERR_CONNECTION_CLOSED); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorAbnormalClosure, _)); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// A connection reset should produce the same event as an unexpected closure. +TEST_F(WebSocketChannelEventInterfaceTest, ConnectionReset) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::ASYNC, + ERR_CONNECTION_RESET); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorAbnormalClosure, _)); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// Connection closed in the middle of a Close message (server bug, etc.) +TEST_F(WebSocketChannelEventInterfaceTest, ConnectionClosedInMessage) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeClose, NOT_MASKED, 7}, + NOT_FINAL_CHUNK, "\x03\xe8"}, + }; + + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks); + stream->PrepareReadFramesError(ReadableFakeWebSocketStream::ASYNC, + ERR_CONNECTION_CLOSED); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorAbnormalClosure, _)); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// RFC6455 5.1 "A client MUST close a connection if it detects a masked frame." +TEST_F(WebSocketChannelEventInterfaceTest, MaskedFramesAreRejected) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, MASKED, 5}, FINAL_CHUNK, + "HELLO"} + }; + + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorProtocolError, _)); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// RFC6455 5.2 "If an unknown opcode is received, the receiving endpoint MUST +// _Fail the WebSocket Connection_." +TEST_F(WebSocketChannelEventInterfaceTest, UnknownOpCodeIsRejected) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + static const InitFrameChunk chunks[] = {{{FINAL_FRAME, 4, NOT_MASKED, 5}, + FINAL_CHUNK, "HELLO"}}; + + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorProtocolError, _)); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// RFC6455 5.4 "Control frames ... MAY be injected in the middle of a +// fragmented message." +TEST_F(WebSocketChannelEventInterfaceTest, ControlFrameInDataMessage) { + scoped_ptr<ReadableFakeWebSocketStream> stream( + new ReadableFakeWebSocketStream); + // We have one message of type Text split into two frames. In the middle is a + // control message of type Pong. + static const InitFrameChunk chunks1[] = { + {{NOT_FINAL_FRAME, WebSocketFrameHeader::kOpCodeText, NOT_MASKED, 6}, + FINAL_CHUNK, "SPLIT "}, + }; + static const InitFrameChunk chunks2[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodePong, NOT_MASKED, 0}, + FINAL_CHUNK, ""} + }; + static const InitFrameChunk chunks3[] = { + {{FINAL_FRAME, WebSocketFrameHeader::kOpCodeContinuation, NOT_MASKED, 7}, + FINAL_CHUNK, "MESSAGE"} + }; + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks1); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks2); + stream->PrepareReadFrames(ReadableFakeWebSocketStream::ASYNC, OK, chunks3); + set_stream(stream.Pass()); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL( + *event_interface_, + OnDataFrame( + false, WebSocketFrameHeader::kOpCodeText, AsVector("SPLIT "))); + EXPECT_CALL(*event_interface_, + OnDataFrame(true, + WebSocketFrameHeader::kOpCodeContinuation, + AsVector("MESSAGE"))); + } + + CreateChannelAndConnectSuccessfully(); + base::MessageLoop::current()->RunUntilIdle(); +} + +// If the renderer sends lots of small writes, we don't want to update the quota +// for each one. +TEST_F(WebSocketChannelEventInterfaceTest, SmallWriteDoesntUpdateQuota) { + set_stream(make_scoped_ptr(new WriteableFakeWebSocketStream)); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + } + + CreateChannelAndConnectSuccessfully(); + channel_->SendFrame(true, WebSocketFrameHeader::kOpCodeText, AsVector("B")); +} + +// If we send enough to go below send_quota_low_water_mask_ we should get our +// quota refreshed. +TEST_F(WebSocketChannelEventInterfaceTest, LargeWriteUpdatesQuota) { + set_stream(make_scoped_ptr(new WriteableFakeWebSocketStream)); + // We use this checkpoint object to verify that the quota update comes after + // the write. + MockFunction<void(int)> checkpoint; + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(1)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(2)); + } + + CreateChannelAndConnectSuccessfully(); + checkpoint.Call(1); + // TODO(ricea): If kDefaultSendQuotaHighWaterMark changes, then this value + // will need to be updated. + channel_->SendFrame( + true, WebSocketFrameHeader::kOpCodeText, std::vector<char>(1 << 17, 'B')); + checkpoint.Call(2); +} + +// Verify that our quota actually is refreshed when we are told it is. +TEST_F(WebSocketChannelEventInterfaceTest, QuotaReallyIsRefreshed) { + set_stream(make_scoped_ptr(new WriteableFakeWebSocketStream)); + MockFunction<void(int)> checkpoint; + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(1)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(2)); + // If quota was not really refreshed, we would get an OnDropChannel() + // message. + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(3)); + } + + CreateChannelAndConnectSuccessfully(); + checkpoint.Call(1); + // TODO(ricea): If kDefaultSendQuotaLowWaterMark and/or + // kDefaultSendQuotaHighWaterMark change, then this value will need to be + // updated. + channel_->SendFrame(true, + WebSocketFrameHeader::kOpCodeText, + std::vector<char>((1 << 16) + 1, 'D')); + checkpoint.Call(2); + // We should have received more quota at this point. + channel_->SendFrame(true, + WebSocketFrameHeader::kOpCodeText, + std::vector<char>((1 << 16) + 1, 'E')); + checkpoint.Call(3); +} + +// If we send more than the available quota then the connection will be closed +// with an error. +TEST_F(WebSocketChannelEventInterfaceTest, WriteOverQuotaIsRejected) { + set_stream(make_scoped_ptr(new WriteableFakeWebSocketStream)); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + // TODO(ricea): Change this if kDefaultSendQuotaHighWaterMark changes. + EXPECT_CALL(*event_interface_, OnFlowControl(1 << 17)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketMuxErrorSendQuotaViolation, _)); + } + + CreateChannelAndConnectSuccessfully(); + channel_->SendFrame(true, + WebSocketFrameHeader::kOpCodeText, + std::vector<char>((1 << 17) + 1, 'C')); +} + +// If a write fails, the channel is dropped. +TEST_F(WebSocketChannelEventInterfaceTest, FailedWrite) { + set_stream(make_scoped_ptr(new UnWriteableFakeWebSocketStream)); + MockFunction<void(int)> checkpoint; + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(checkpoint, Call(1)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketErrorAbnormalClosure, _)); + EXPECT_CALL(checkpoint, Call(2)); + } + + CreateChannelAndConnectSuccessfully(); + checkpoint.Call(1); + + channel_->SendFrame(true, WebSocketFrameHeader::kOpCodeText, AsVector("H")); + checkpoint.Call(2); +} + +// OnDropChannel() is called exactly once when StartClosingHandshake() is used. +TEST_F(WebSocketChannelEventInterfaceTest, SendCloseDropsChannel) { + set_stream(make_scoped_ptr(new EchoeyFakeWebSocketStream)); + { + InSequence s; + EXPECT_CALL(*event_interface_, OnAddChannelResponse(false, _)); + EXPECT_CALL(*event_interface_, OnFlowControl(_)); + EXPECT_CALL(*event_interface_, + OnDropChannel(kWebSocketNormalClosure, "Fred")); + } + + CreateChannelAndConnectSuccessfully(); + + channel_->StartClosingHandshake(kWebSocketNormalClosure, "Fred"); + base::MessageLoop::current()->RunUntilIdle(); +} + +// RFC6455 5.1 "a client MUST mask all frames that it sends to the server". +// WebSocketChannel actually only sets the mask bit in the header, it doesn't +// perform masking itself (not all transports actually use masking). +TEST_F(WebSocketChannelStreamTest, SentFramesAreMasked) { + EXPECT_CALL(*mock_stream_, GetSubProtocol()).Times(AnyNumber()); + EXPECT_CALL(*mock_stream_, ReadFrames(_, _)).WillOnce(Return(ERR_IO_PENDING)); + EXPECT_CALL( + *mock_stream_, + WriteFrames(Pointee(ElementsAre(Pointee(Field( + &WebSocketFrameChunk::header, + Pointee(Field(&WebSocketFrameHeader::masked, true)))))), + _)).WillOnce(Return(ERR_IO_PENDING)); + + CreateChannelAndConnectSuccessfully(); + channel_->SendFrame( + true, WebSocketFrameHeader::kOpCodeText, AsVector("NEEDS MASKING")); +} + +} // namespace +} // namespace net diff --git a/net/websockets/websocket_event_interface.h b/net/websockets/websocket_event_interface.h new file mode 100644 index 0000000..7eb54bf --- /dev/null +++ b/net/websockets/websocket_event_interface.h @@ -0,0 +1,76 @@ +// 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_EVENT_INTERFACE_H_ +#define NET_WEBSOCKETS_WEBSOCKET_EVENT_INTERFACE_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "net/base/net_export.h" + +namespace net { + +// Interface for events sent from the network layer to the content layer. These +// events will generally be sent as-is to the renderer process. +class NET_EXPORT WebSocketEventInterface { + public: + typedef int WebSocketMessageType; + virtual ~WebSocketEventInterface() {} + // Called in response to an AddChannelRequest. This generally means that a + // response has been received from the remote server, but the response might + // have been generated internally. If |fail| is true, the channel cannot be + // used and it is valid to delete the WebSocketChannel from within this + // callback. + virtual void OnAddChannelResponse( + bool fail, + const std::string& selected_subprotocol) = 0; + + // Called when a data frame has been received from the remote host and needs + // to be forwarded to the renderer process. It is not safe to delete the + // WebSocketChannel object from within this callback. + virtual void OnDataFrame(bool fin, + WebSocketMessageType type, + const std::vector<char>& data) = 0; + + // Called to provide more send quota for this channel to the renderer + // process. Currently the quota units are always bytes of message body + // data. In future it might depend on the type of multiplexing in use. It is + // not safe to delete the WebSocketChannel from within this callback. + virtual void OnFlowControl(int64 quota) = 0; + + // Called when the remote server has Started the WebSocket Closing + // Handshake. The client should not attempt to send any more messages after + // receiving this message. It will be followed by OnDropChannel() when the + // closing handshake is complete. It is not safe to delete the + // WebSocketChannel from within this callback. + virtual void OnClosingHandshake() = 0; + + // Called when the channel has been dropped, either due to a network close, a + // network error, or a protocol error. This may or may not be preceeded by a + // call to OnClosingHandshake(). + // + // Warning: Both the |code| and |reason| are passed through to Javascript, so + // callers must take care not to provide details that could be useful to + // attackers attempting to use WebSockets to probe networks. + // + // The channel should not be used again after OnDropChannel() has been + // called. + // + // It is not safe to delete the WebSocketChannel from within this + // callback. It is recommended to delete the channel after returning to the + // event loop. + virtual void OnDropChannel(uint16 code, const std::string& reason) = 0; + + protected: + WebSocketEventInterface() {} + + private: + DISALLOW_COPY_AND_ASSIGN(WebSocketEventInterface); +}; + +} // namespace net + +#endif // NET_WEBSOCKETS_WEBSOCKET_EVENT_INTERFACE_H_ diff --git a/net/websockets/websocket_mux.h b/net/websockets/websocket_mux.h new file mode 100644 index 0000000..9fc1f67 --- /dev/null +++ b/net/websockets/websocket_mux.h @@ -0,0 +1,39 @@ +// 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_MUX_H_ +#define NET_WEBSOCKETS_WEBSOCKET_MUX_H_ + +namespace net { + +// Reason codes used by the mux extension. +enum WebSocketMuxError { + // Codes starting with 2000 apply to the physical connection. They are used + // for dropping the control channel. + kWebSocketMuxErrorPhysicalConnectionFailed = 2000, + kWebSocketMuxErrorInvalidEncapsulatingMessage = 2001, + kWebSocketMuxErrorChannelIdTruncated = 2002, + kWebSocketMuxErrorEncapsulatedFrameIsTruncated = 2003, + kWebSocketMuxErrorUnknownMuxOpcode = 2004, + kWebSocketMuxErrorInvalidMuxControlBlock = 2005, + kWebSocketMuxErrorChannelAlreadyExists = 2006, + kWebSocketMuxErrorNewChannelSlotViolation = 2007, + kWebSocketMuxErrorNewChannelSlotOverflow = 2008, + kWebSocketMuxErrorBadRequest = 2009, + kWebSocketMuxErrorUnknownRequestEncoding = 2010, + kWebSocketMuxErrorBadResponse = 2011, + kWebSocketMuxErrorUnknownResponseEncoding = 2012, + + // Codes starting with 3000 apply to the logical connection. + kWebSocketMuxErrorLogicalChannelFailed = 3000, + kWebSocketMuxErrorSendQuotaViolation = 3005, + kWebSocketMuxErrorSendQuotaOverflow = 3006, + kWebSocketMuxErrorIdleTimeout = 3007, + kWebSocketMuxErrorDropChannelAck = 3008, + kWebSocketMuxErrorBadFragmentation = 3009, +}; + +} // namespace net + +#endif // NET_WEBSOCKETS_WEBSOCKET_MUX_H_ |