blob: d81dcf4d77dda631dcf3362b38d03495d8de1dae [file] [log] [blame]
// Copyright 2017 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/websocket/web_socket.h"
#include <set>
#include <string>
#include <vector>
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "cobalt/base/polymorphic_downcast.h"
#include "cobalt/dom/document.h"
#include "cobalt/dom/dom_settings.h"
#include "cobalt/dom/window.h"
#include "cobalt/script/global_environment.h"
#include "cobalt/websocket/close_event.h"
#include "net/base/port_util.h"
#include "net/base/url_util.h"
#include "net/websockets/websocket_errors.h"
#include "url/gurl.h"
#include "url/url_canon.h"
#include "url/url_constants.h"
namespace {
typedef uint16 SerializedCloseStatusCodeType;
static const std::string kComma = ",";
bool IsURLAbsolute(cobalt::dom::DOMSettings* dom_settings,
const std::string& url) {
// This is a requirement for calling spec()
DCHECK(dom_settings->base_url().is_valid());
url::RawCanonOutputT<char> whitespace_buffer;
int relative_length;
bool potentially_dangling_markup;
const char* relative = RemoveURLWhitespace(
url.c_str(), static_cast<int>(url.size()), &whitespace_buffer,
&relative_length, &potentially_dangling_markup);
url::Component relative_component;
const std::string& base_url(dom_settings->base_url().spec());
url::Parsed parsed;
url::ParseStandardURL(base_url.c_str(), static_cast<int>(base_url.length()),
&parsed);
bool is_relative;
url::IsRelativeURL(base_url.c_str(), parsed, relative, relative_length, true,
&is_relative, &relative_component);
return !is_relative;
}
bool IsValidSubprotocolCharacter(const char subprotocol_char) {
// From RFC 2616 (https://tools.ietf.org/html/rfc2616),
// CHAR: "any US-ASCII character (octets 0 - 127)"
// CTL: "any US-ASCII control character (octets 0 - 31) and DEL (127)>"
bool is_CHAR_except_CTL =
((subprotocol_char > 31) && (subprotocol_char < 127));
bool is_separator = false;
// Per spec, the separators are defined as following where (SP and HT are
// space and horizontal tab, respectively):
// separators = "(" | ")" | "<" | ">" | "@"
// | "," | ";" | ":" | "\" | <">
// | "/" | "[" | "]" | "?" | "="
// | "{" | "}" | SP | HT
switch (subprotocol_char) {
case '(':
case ')':
case '<':
case '>':
case '@':
case ',':
case ';':
case ':':
case '\\':
case '"':
case '/':
case '[':
case ']':
case '?':
case '=':
case '{':
case '}':
case ' ':
case '\t':
is_separator = true;
break;
default:
break;
}
return (is_CHAR_except_CTL && !is_separator);
}
bool IsSubProtocolValid(const std::string& sub_protocol) {
if (sub_protocol.empty()) {
return false;
}
for (std::string::const_iterator it(sub_protocol.begin());
it != sub_protocol.end(); ++it) {
if (!IsValidSubprotocolCharacter(*it)) {
return false;
}
}
return true;
}
bool AreSubProtocolsValid(const std::vector<std::string>& sub_protocols,
std::string* invalid_subprotocol) {
DCHECK(invalid_subprotocol);
for (std::vector<std::string>::const_iterator it(sub_protocols.begin());
it != sub_protocols.end(); ++it) {
if (!IsSubProtocolValid(*it)) {
*invalid_subprotocol = *it;
return false;
}
}
return true;
}
bool AreSubProtocolsUnique(const std::vector<std::string>& sub_protocols) {
if (sub_protocols.size() <= 1) return true;
std::set<std::string> all_protocols;
all_protocols.insert(sub_protocols.begin(), sub_protocols.end());
return (all_protocols.size() == sub_protocols.size());
}
bool IsValidBinaryType(cobalt::dom::MessageEvent::ResponseTypeCode code) {
return (code == cobalt::dom::MessageEvent::kBlob) ||
(code == cobalt::dom::MessageEvent::kArrayBuffer);
}
} // namespace
namespace cobalt {
namespace websocket {
const uint16 WebSocket::kConnecting;
const uint16 WebSocket::kOpen;
const uint16 WebSocket::kClosing;
const uint16 WebSocket::kClosed;
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket.
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url,
script::ExceptionState* exception_state)
: dom::EventTarget(settings), require_network_module_(true) {
const std::vector<std::string> empty{};
Initialize(settings, url, empty, exception_state);
}
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url,
const std::vector<std::string>& sub_protocols,
script::ExceptionState* exception_state)
: dom::EventTarget(settings), require_network_module_(true) {
Initialize(settings, url, sub_protocols, exception_state);
}
WebSocket::~WebSocket() {
if (impl_) {
impl_->ResetWebSocketEventDelegate();
}
}
std::string WebSocket::binary_type(script::ExceptionState* exception_state) {
if (!IsValidBinaryType(binary_type_)) {
NOTREACHED() << "Invalid binary_type_";
dom::DOMException::Raise(dom::DOMException::kNone, exception_state);
return std::string();
}
return dom::MessageEvent::GetResponseTypeAsString(binary_type_);
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket.
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url,
const std::string& sub_protocol_list,
script::ExceptionState* exception_state)
: dom::EventTarget(settings), require_network_module_(true) {
std::vector<std::string> sub_protocols =
base::SplitString(sub_protocol_list, kComma, base::KEEP_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
Initialize(settings, url, sub_protocols, exception_state);
}
void WebSocket::set_binary_type(const std::string& binary_type,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Per spec:
// "On setting, if the new value is either the string "blob" or the string
// "arraybuffer", then set the IDL attribute to this new value.
// Otherwise, throw a SyntaxError exception."
base::StringPiece binary_type_string_piece(binary_type);
dom::MessageEvent::ResponseTypeCode response_code =
dom::MessageEvent::GetResponseTypeCode(binary_type_string_piece);
if (!IsValidBinaryType(response_code)) {
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, exception_state);
} else {
binary_type_ = response_code;
}
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-close.
void WebSocket::Close(script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
const std::string empty_reason;
Close(net::kWebSocketNormalClosure, empty_reason, exception_state);
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-close.
void WebSocket::Close(const uint16 code,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
const std::string empty_reason;
Close(code, empty_reason, exception_state);
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-close.
void WebSocket::Close(const uint16 code, const std::string& reason,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Per spec @ https://www.w3.org/TR/websockets/#dom-websocket-close
// "If reason is longer than 123 bytes, then throw a SyntaxError exception and
// abort these steps."
if (reason.size() > kMaxCloseReasonSize) {
DLOG(ERROR) << "Reason specified in WebSocket::Close must be less than "
<< kMaxControlPayloadSizeInBytes << " bytes.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, exception_state);
return;
}
if (net::WebSocketErrorToNetError(static_cast<net::WebSocketError>(code)) ==
net::ERR_UNEXPECTED) {
dom::DOMException::Raise(dom::DOMException::kInvalidAccessErr,
exception_state);
return;
}
DLOG(INFO) << "Websocket close code " << code;
switch (ready_state()) {
case kOpen:
case kConnecting:
DCHECK(impl_);
impl_->Close(net::WebSocketError(code), reason);
SetReadyState(kClosing);
break;
case kClosing:
case kClosed:
return;
default:
NOTREACHED() << "Invalid ready_state_ " << ready_state()
<< " in WebSocket::Close.";
}
}
bool WebSocket::CheckReadyState(script::ExceptionState* exception_state) {
DCHECK(exception_state);
// Per Websockets API spec:
// "If the readyState attribute is CONNECTING, it must throw an
// InvalidStateError exception"
if (ready_state() == kConnecting) {
dom::DOMException::Raise(dom::DOMException::kInvalidStateErr,
"readyState is not in CONNECTING state",
exception_state);
return false;
}
return true;
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-send.
void WebSocket::Send(const std::string& data,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(impl_);
if (!CheckReadyState(exception_state)) {
return;
}
std::string error_message;
bool success = impl_->SendText(data.data(), data.size(), &buffered_amount_,
&error_message);
if (!success) {
DLOG(ERROR) << "Unable to send message: [" << error_message << "]";
}
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-send.
void WebSocket::Send(const scoped_refptr<dom::Blob>& data,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(impl_);
if (!CheckReadyState(exception_state)) {
return;
}
dom::Blob* blob(data.get());
if (!blob) {
return;
}
std::string error_message;
bool success = impl_->SendBinary(reinterpret_cast<const char*>(blob->data()),
static_cast<size_t>(blob->size()),
&buffered_amount_, &error_message);
if (!success) {
DLOG(ERROR) << "Unable to send message: [" << error_message << "]";
}
}
// Implements spec at https://www.w3.org/TR/websockets/#dom-websocket-send.
void WebSocket::Send(const script::Handle<script::ArrayBuffer>& data,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(impl_);
if (!CheckReadyState(exception_state)) {
return;
}
std::string error_message;
if (data.IsEmpty()) {
return;
}
bool success =
impl_->SendBinary(reinterpret_cast<const char*>(data->Data()),
data->ByteLength(), &buffered_amount_, &error_message);
if (!success) {
DLOG(ERROR) << "Unable to send message: [" << error_message << "]";
}
}
// Implements spect at https://www.w3.org/TR/websockets/#dom-websocket-send.
void WebSocket::Send(const script::Handle<script::ArrayBufferView>& data,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(impl_);
if (!CheckReadyState(exception_state)) {
return;
}
std::string error_message;
if (data.IsEmpty()) {
return;
}
bool success =
impl_->SendBinary(reinterpret_cast<const char*>(data->RawData()),
data->ByteLength(), &buffered_amount_, &error_message);
if (!success) {
DLOG(ERROR) << "Unable to send message: [" << error_message << "]";
}
}
std::string WebSocket::GetResourceName() const {
if (resolved_url_.has_query()) {
return resolved_url_.path() + "?" + resolved_url_.query();
} else {
return resolved_url_.path();
}
}
std::string WebSocket::GetPortAsString() const {
if (resolved_url_.has_port()) {
return resolved_url_.port();
}
return base::IntToString(GetPort());
}
void WebSocket::OnConnected(const std::string& selected_subprotocol) {
DLOG(INFO) << "Websockets selected subprotocol: [" << selected_subprotocol
<< "]";
protocol_ = selected_subprotocol;
SetReadyState(kOpen);
this->DispatchEvent(new dom::Event(base::Tokens::open()));
}
void WebSocket::OnDisconnected(bool was_clean, uint16 code,
const std::string& reason) {
SetReadyState(kClosed);
CloseEventInit close_event_init;
close_event_init.set_was_clean(was_clean);
close_event_init.set_code(code);
close_event_init.set_reason(reason);
this->DispatchEvent(new CloseEvent(base::Tokens::close(), close_event_init));
}
void WebSocket::OnReceivedData(bool is_text_frame,
scoped_refptr<net::IOBufferWithSize> data) {
dom::MessageEvent::ResponseTypeCode response_type_code = binary_type_;
if (is_text_frame) {
response_type_code = dom::MessageEvent::kText;
}
this->DispatchEvent(new dom::MessageEvent(base::Tokens::message(), settings_,
response_type_code, data));
}
void WebSocket::OnWriteDone(uint64_t bytes_written) {
buffered_amount_ -= bytes_written;
}
void WebSocket::Initialize(script::EnvironmentSettings* settings,
const std::string& url,
const std::vector<std::string>& sub_protocols,
script::ExceptionState* exception_state) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
buffered_amount_ = 0;
binary_type_ = dom::MessageEvent::kBlob;
is_secure_ = false;
port_ = -1;
SetReadyState(kConnecting);
settings_ = base::polymorphic_downcast<dom::DOMSettings*>(settings);
if (!settings_) {
dom::DOMException::Raise(dom::DOMException::kNone,
"Internal error: Unable to get DOM settings.",
exception_state);
NOTREACHED() << "Unable to get DOM settings.";
return;
}
if (require_network_module_ && !settings_->network_module()) {
dom::DOMException::Raise(dom::DOMException::kNone,
"Internal error: Unable to get network module.",
exception_state);
NOTREACHED() << "Unable to get network module.";
return;
}
if (!settings_->base_url().is_valid()) {
dom::DOMException::Raise(
dom::DOMException::kNone,
"Internal error: base_url (the url of the entry script) must be valid.",
exception_state);
return;
} else {
// GetOrigin() can only be called on valid urls.
// Since origin does not contain fragments, spec() is guaranteed
// to return an ASCII encoded string.
entry_script_origin_ = settings_->base_url().GetOrigin().spec();
}
// Per spec:
// Parse a WebSocket URL's components from the url argument, to obtain host,
// port, resource name, and secure. If this fails, throw a SyntaxError
// exception and abort these steps. [WSP]"
resolved_url_ = settings_->base_url().Resolve(url);
if (resolved_url_.is_empty()) {
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, "url is empty",
exception_state);
return;
}
if (!resolved_url_.is_valid()) {
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, "url is invalid",
exception_state);
return;
}
bool is_absolute = IsURLAbsolute(settings_, url);
if (!is_absolute) {
std::string error_message = "Only relative URLs are supported. [" + url +
"] is not an absolute URL.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, error_message,
exception_state);
return;
}
// Per spec @ https://www.w3.org/TR/websockets/#parsing-websocket-urls:
// If url does not have a <scheme> component whose value, when converted to
// ASCII lowercase, is either "ws" or "wss", then fail this algorithm.
if (resolved_url_.SchemeIs(url::kWsScheme)) {
is_secure_ = false;
} else if (resolved_url_.SchemeIs(url::kWssScheme)) {
is_secure_ = true;
} else {
std::string error_message = "Invalid scheme [" + resolved_url_.scheme() +
"]. Only " + std::string(url::kWsScheme) +
", and " + std::string(url::kWssScheme) +
" schemes are supported.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, error_message,
exception_state);
return;
}
// If url has a <fragment> component, then fail this algorithm.
std::string fragment(resolved_url_.ref());
if (!fragment.empty()) {
std::string error_message =
"URL has a fragment '" + fragment +
"'. Fragments are not are supported in websocket URLs.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, error_message,
exception_state);
return;
}
dom::CspDelegate* csp = csp_delegate();
if (csp &&
!csp->CanLoad(dom::CspDelegate::kWebSocket, resolved_url_, false)) {
dom::DOMException::Raise(dom::DOMException::kSecurityErr, exception_state);
return;
}
if (!net::IsPortAllowedForScheme(GetPort(), resolved_url_.scheme())) {
std::string error_message = "Connecting to port " + GetPortAsString() +
" using websockets is not allowed.";
dom::DOMException::Raise(dom::DOMException::kSecurityErr, error_message,
exception_state);
return;
}
std::string invalid_subprotocol;
if (!AreSubProtocolsValid(sub_protocols, &invalid_subprotocol)) {
std::string error_message = "Invalid subprotocol [" + invalid_subprotocol +
"]. Subprotocols' characters must be in valid "
"range and not have separating characters. "
"See RFC 2616 for details.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, error_message,
exception_state);
return;
}
if (!AreSubProtocolsUnique(sub_protocols)) {
std::string error_message = "Subprotocol values must be unique.";
dom::DOMException::Raise(dom::DOMException::kSyntaxErr, error_message,
exception_state);
return;
}
Connect(resolved_url_, sub_protocols);
}
dom::CspDelegate* WebSocket::csp_delegate() const {
DCHECK(settings_);
if (!settings_) {
return NULL;
}
if (settings_->window() && settings_->window()->document()) {
return settings_->window()->document()->csp_delegate();
} else {
return NULL;
}
}
void WebSocket::Connect(const GURL& url,
const std::vector<std::string>& sub_protocols) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(settings_);
GURL origin_gurl = settings_->base_url().GetOrigin();
const std::string& origin = origin_gurl.possibly_invalid_spec();
impl_ = base::WrapRefCounted(
new WebSocketImpl(settings_->network_module(), this));
impl_->Connect(origin, url, sub_protocols);
}
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url,
script::ExceptionState* exception_state,
const bool require_network_module)
: dom::EventTarget(settings),
require_network_module_(require_network_module) {
const std::vector<std::string> empty{};
Initialize(settings, url, empty, exception_state);
}
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url, const std::string& sub_protocol,
script::ExceptionState* exception_state,
const bool require_network_module)
: dom::EventTarget(settings),
require_network_module_(require_network_module) {
std::vector<std::string> sub_protocols;
sub_protocols.push_back(sub_protocol);
Initialize(settings, url, sub_protocols, exception_state);
}
WebSocket::WebSocket(script::EnvironmentSettings* settings,
const std::string& url,
const std::vector<std::string>& sub_protocols,
script::ExceptionState* exception_state,
const bool require_network_module)
: dom::EventTarget(settings),
require_network_module_(require_network_module) {
Initialize(settings, url, sub_protocols, exception_state);
}
void WebSocket::PotentiallyAllowGarbageCollection() {
bool prevent_gc = false;
switch (ready_state()) {
case kOpen:
// Per spec, "A WebSocket object whose readyState attribute's value was
// set to OPEN (1) as of the last time the event loop started executing a
// task must not be garbage collected if there are any event listeners
// registered for message events, error, or close events..
// A WebSocket object with an established connection that has data queued
// to be transmitted to the network must not be garbage collected."
prevent_gc = HasOnMessageListener() || HasOnErrorListener() ||
HasOnCloseListener() || HasOutstandingData();
break;
case kConnecting:
// Per spec, "WebSocket object whose readyState attribute's value was set
// to CONNECTING (0) as of the last time the event loop started executing
// a task must not be garbage collected if there are any event listeners
// registered for open events, message events, error events, or close
// events."
prevent_gc = HasOnOpenListener() || HasOnMessageListener() ||
HasOnErrorListener() || HasOnCloseListener() ||
HasOutstandingData();
break;
case kClosing:
// Per spec, "A WebSocket object whose readyState attribute's value was
// set to CLOSING (2) as of the last time the event loop started
// executing a task must not be garbage collected if there are any event
// listeners registered for error or close events."
prevent_gc =
HasOnErrorListener() || HasOnCloseListener() || HasOutstandingData();
break;
case kClosed:
prevent_gc = false;
break;
default:
NOTREACHED() << "Invalid ready_state: " << ready_state();
}
if (prevent_gc != (prevent_gc_while_listening_ != NULL)) {
if (prevent_gc) {
prevent_gc_while_listening_.reset(
new script::GlobalEnvironment::ScopedPreventGarbageCollection(
settings_->global_environment(), this));
} else {
// Note: the fall through in this switch statement is on purpose.
switch (ready_state_) {
case kConnecting:
DCHECK(!HasOnOpenListener());
case kOpen:
DCHECK(!HasOnMessageListener());
case kClosing:
DCHECK(!HasOnErrorListener());
DCHECK(!HasOnCloseListener());
default:
break;
}
prevent_gc_while_listening_.reset();
}
// The above function calls should change |(prevent_gc_while_listening_ !=
// NULL)|.
DCHECK_EQ(prevent_gc, (prevent_gc_while_listening_ != NULL));
}
}
} // namespace websocket
} // namespace cobalt