Commit d64df5e4 authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Split shared code into WebSocketCommon

Split out code from DOMWebSocket that will be shared with
WebSocketStream into a new class, WebSocketCommon.

The major methods that have been moved are Connect() and
CloseInternal(). Connect() in particular has some security-critical
checks that should not be duplicated.

Some members of DOMWebSocket have been moved to WebSocketCommon because
they need to be modified by WebSocketCommon methods. The
most important of these is |status_|.

This is part of the implementation work for WebSocketStream. The design
doc for the work as a whole is at
https://docs.google.com/document/d/1XuxEshh5VYBYm1qRVKordTamCOsR-uGQBCYFcHXP4L0/edit

BUG=983030

Change-Id: Idba0d5181b2a6946f65d10083f9e15a0fe5e7ec9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1731010Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/master@{#683938}
parent 915b8fea
...@@ -417,6 +417,7 @@ jumbo_source_set("unit_tests") { ...@@ -417,6 +417,7 @@ jumbo_source_set("unit_tests") {
"webshare/navigator_share_test.cc", "webshare/navigator_share_test.cc",
"websockets/dom_websocket_test.cc", "websockets/dom_websocket_test.cc",
"websockets/websocket_channel_impl_test.cc", "websockets/websocket_channel_impl_test.cc",
"websockets/websocket_common_test.cc",
"worklet/animation_and_paint_worklet_thread_test.cc", "worklet/animation_and_paint_worklet_thread_test.cc",
"worklet/worklet_thread_test_common.cc", "worklet/worklet_thread_test_common.cc",
"worklet/worklet_thread_test_common.h", "worklet/worklet_thread_test_common.h",
......
...@@ -19,6 +19,8 @@ blink_modules_sources("websockets") { ...@@ -19,6 +19,8 @@ blink_modules_sources("websockets") {
"websocket_channel_client.h", "websocket_channel_client.h",
"websocket_channel_impl.cc", "websocket_channel_impl.cc",
"websocket_channel_impl.h", "websocket_channel_impl.h",
"websocket_common.cc",
"websocket_common.h",
"websocket_handle.h", "websocket_handle.h",
"websocket_handle_client.h", "websocket_handle_client.h",
"websocket_handle_impl.cc", "websocket_handle_impl.cc",
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
#include "third_party/blink/renderer/modules/websockets/websocket_channel.h" #include "third_party/blink/renderer/modules/websockets/websocket_channel.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel_client.h" #include "third_party/blink/renderer/modules/websockets/websocket_channel_client.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel_impl.h" #include "third_party/blink/renderer/modules/websockets/websocket_channel_impl.h"
#include "third_party/blink/renderer/modules/websockets/websocket_common.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/heap/handle.h" #include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/timer.h" #include "third_party/blink/renderer/platform/timer.h"
...@@ -69,6 +70,12 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -69,6 +70,12 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
USING_GARBAGE_COLLECTED_MIXIN(DOMWebSocket); USING_GARBAGE_COLLECTED_MIXIN(DOMWebSocket);
public: public:
// These definitions are required by V8DOMWebSocket.
static constexpr auto kConnecting = WebSocketCommon::kConnecting;
static constexpr auto kOpen = WebSocketCommon::kOpen;
static constexpr auto kClosing = WebSocketCommon::kClosing;
static constexpr auto kClosed = WebSocketCommon::kClosed;
// DOMWebSocket instances must be used with a wrapper since this class's // DOMWebSocket instances must be used with a wrapper since this class's
// lifetime management is designed assuming the V8 holds a ref on it while // lifetime management is designed assuming the V8 holds a ref on it while
// hasPendingActivity() returns true. // hasPendingActivity() returns true.
...@@ -83,8 +90,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -83,8 +90,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
explicit DOMWebSocket(ExecutionContext*); explicit DOMWebSocket(ExecutionContext*);
~DOMWebSocket() override; ~DOMWebSocket() override;
enum State { kConnecting = 0, kOpen = 1, kClosing = 2, kClosed = 3 };
void Connect(const String& url, void Connect(const String& url,
const Vector<String>& protocols, const Vector<String>& protocols,
ExceptionState&); ExceptionState&);
...@@ -104,7 +109,7 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -104,7 +109,7 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
void close(uint16_t code, ExceptionState&); void close(uint16_t code, ExceptionState&);
const KURL& url() const; const KURL& url() const;
State readyState() const; WebSocketCommon::State readyState() const;
uint64_t bufferedAmount() const; uint64_t bufferedAmount() const;
String protocol() const; String protocol() const;
...@@ -145,8 +150,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -145,8 +150,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
void Trace(blink::Visitor*) override; void Trace(blink::Visitor*) override;
static bool IsValidSubprotocolString(const String&);
private: private:
// FIXME: This should inherit blink::EventQueue. // FIXME: This should inherit blink::EventQueue.
class EventQueue final : public GarbageCollectedFinalized<EventQueue> { class EventQueue final : public GarbageCollectedFinalized<EventQueue> {
...@@ -247,9 +250,8 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -247,9 +250,8 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
Member<WebSocketChannel> channel_; Member<WebSocketChannel> channel_;
State state_; WebSocketCommon common_;
KURL url_;
String origin_string_; String origin_string_;
uint64_t buffered_amount_; uint64_t buffered_amount_;
...@@ -265,8 +267,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData, ...@@ -265,8 +267,6 @@ class MODULES_EXPORT DOMWebSocket : public EventTargetWithInlineData,
Member<EventQueue> event_queue_; Member<EventQueue> event_queue_;
bool buffered_amount_update_task_pending_; bool buffered_amount_update_task_pending_;
bool was_autoupgraded_to_wss_;
}; };
} // namespace blink } // namespace blink
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#include "third_party/blink/renderer/modules/websockets/dom_websocket.h" #include "third_party/blink/renderer/modules/websockets/dom_websocket.h"
#include <memory> #include <memory>
#include <string>
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
...@@ -345,31 +346,6 @@ TEST(DOMWebSocketTest, channelConnectFail) { ...@@ -345,31 +346,6 @@ TEST(DOMWebSocketTest, channelConnectFail) {
EXPECT_EQ(DOMWebSocket::kClosed, websocket_scope.Socket().readyState()); EXPECT_EQ(DOMWebSocket::kClosed, websocket_scope.Socket().readyState());
} }
TEST(DOMWebSocketTest, isValidSubprotocolString) {
EXPECT_TRUE(DOMWebSocket::IsValidSubprotocolString("Helloworld!!"));
EXPECT_FALSE(DOMWebSocket::IsValidSubprotocolString("Hello, world!!"));
EXPECT_FALSE(DOMWebSocket::IsValidSubprotocolString(String()));
EXPECT_FALSE(DOMWebSocket::IsValidSubprotocolString(""));
const char kValidCharacters[] =
"!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`"
"abcdefghijklmnopqrstuvwxyz|~";
size_t length = strlen(kValidCharacters);
for (size_t i = 0; i < length; ++i) {
String s(kValidCharacters + i, 1u);
EXPECT_TRUE(DOMWebSocket::IsValidSubprotocolString(s));
}
for (size_t i = 0; i < 256; ++i) {
if (std::find(kValidCharacters, kValidCharacters + length,
static_cast<char>(i)) != kValidCharacters + length) {
continue;
}
char to_check = char{i};
String s(&to_check, 1u);
EXPECT_FALSE(DOMWebSocket::IsValidSubprotocolString(s));
}
}
TEST(DOMWebSocketTest, connectSuccess) { TEST(DOMWebSocketTest, connectSuccess) {
V8TestingScope scope; V8TestingScope scope;
DOMWebSocketTestScope websocket_scope(scope.GetExecutionContext()); DOMWebSocketTestScope websocket_scope(scope.GetExecutionContext());
......
// Copyright 2019 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 "third_party/blink/renderer/modules/websockets/websocket_common.h"
#include <stddef.h>
#include "base/metrics/histogram_macros.h"
#include "third_party/blink/public/platform/web_insecure_request_policy.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/loader/mixed_content_checker.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/loader/mixed_content_autoupgrade_status.h"
#include "third_party/blink/renderer/modules/websockets/websocket_channel.h"
#include "third_party/blink/renderer/platform/weborigin/known_ports.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
namespace blink {
namespace {
constexpr char kWebSocketSubprotocolSeparator[] = ", ";
constexpr size_t kMaxReasonSizeInBytes = 123;
} // namespace
WebSocketCommon::ConnectResult WebSocketCommon::Connect(
ExecutionContext* execution_context,
const String& url,
const Vector<String>& protocols,
WebSocketChannel* channel,
ExceptionState& exception_state) {
url_ = KURL(NullURL(), url);
bool upgrade_insecure_requests_set =
execution_context->GetSecurityContext().GetInsecureRequestPolicy() &
kUpgradeInsecureRequests;
if ((upgrade_insecure_requests_set ||
MixedContentChecker::ShouldAutoupgrade(
execution_context->GetHttpsState(),
WebMixedContentContextType::kBlockable)) &&
url_.Protocol() == "ws" &&
!SecurityOrigin::Create(url_)->IsPotentiallyTrustworthy()) {
if (!upgrade_insecure_requests_set) {
was_autoupgraded_to_wss_ = true;
LogMixedAutoupgradeStatus(MixedContentAutoupgradeStatus::kStarted);
execution_context->AddConsoleMessage(
MixedContentChecker::CreateConsoleMessageAboutWebSocketAutoupgrade(
execution_context->Url(), url_));
}
UseCounter::Count(execution_context,
WebFeature::kUpgradeInsecureRequestsUpgradedRequest);
url_.SetProtocol("wss");
if (url_.Port() == 80)
url_.SetPort(443);
}
if (!url_.IsValid()) {
state_ = kClosed;
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"The URL '" + url + "' is invalid.");
return ConnectResult::kException;
}
if (!url_.ProtocolIs("ws") && !url_.ProtocolIs("wss")) {
state_ = kClosed;
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"The URL's scheme must be either 'ws' or 'wss'. '" + url_.Protocol() +
"' is not allowed.");
return ConnectResult::kException;
}
if (url_.HasFragmentIdentifier()) {
state_ = kClosed;
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"The URL contains a fragment identifier ('" +
url_.FragmentIdentifier() +
"'). Fragment identifiers are not allowed in WebSocket URLs.");
return ConnectResult::kException;
}
if (!IsPortAllowedForScheme(url_)) {
state_ = kClosed;
exception_state.ThrowSecurityError(
"The port " + String::Number(url_.Port()) + " is not allowed.");
return ConnectResult::kException;
}
if (!execution_context->GetContentSecurityPolicyForWorld()
->AllowConnectToSource(url_)) {
state_ = kClosed;
return ConnectResult::kAsyncError;
}
// Fail if not all elements in |protocols| are valid.
for (const String& protocol : protocols) {
if (!IsValidSubprotocolString(protocol)) {
state_ = kClosed;
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"The subprotocol '" +
EncodeSubprotocolString(protocol) +
"' is invalid.");
return ConnectResult::kException;
}
}
// Fail if there're duplicated elements in |protocols|.
HashSet<String> visited;
for (const String& protocol : protocols) {
if (!visited.insert(protocol).is_new_entry) {
state_ = kClosed;
exception_state.ThrowDOMException(DOMExceptionCode::kSyntaxError,
"The subprotocol '" +
EncodeSubprotocolString(protocol) +
"' is duplicated.");
return ConnectResult::kException;
}
}
String protocol_string;
if (!protocols.IsEmpty())
protocol_string = JoinStrings(protocols, kWebSocketSubprotocolSeparator);
if (!channel->Connect(url_, protocol_string)) {
state_ = kClosed;
exception_state.ThrowSecurityError(
"An insecure WebSocket connection may not be initiated from a page "
"loaded over HTTPS.");
channel->Disconnect();
return ConnectResult::kException;
}
return ConnectResult::kSuccess;
}
void WebSocketCommon::CloseInternal(int code,
const String& reason,
WebSocketChannel* channel,
ExceptionState& exception_state) {
String cleansed_reason = reason;
if (code == WebSocketChannel::kCloseEventCodeNotSpecified) {
DVLOG(1) << "WebSocket " << this << " close() without code and reason";
} else {
DVLOG(1) << "WebSocket " << this << " close() code=" << code
<< " reason=" << reason;
if (!(code == WebSocketChannel::kCloseEventCodeNormalClosure ||
(WebSocketChannel::kCloseEventCodeMinimumUserDefined <= code &&
code <= WebSocketChannel::kCloseEventCodeMaximumUserDefined))) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidAccessError,
"The code must be either 1000, or between 3000 and 4999. " +
String::Number(code) + " is neither.");
return;
}
// Bindings specify USVString, so unpaired surrogates are already replaced
// with U+FFFD.
StringUTF8Adaptor utf8(reason);
if (utf8.size() > kMaxReasonSizeInBytes) {
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"The message must not be greater than " +
String::Number(kMaxReasonSizeInBytes) + " bytes.");
return;
}
if (!reason.IsEmpty() && !reason.Is8Bit()) {
DCHECK_GT(utf8.size(), 0u);
// reason might contain unpaired surrogates. Reconstruct it from
// utf8.
cleansed_reason = String::FromUTF8(utf8.data(), utf8.size());
}
}
if (state_ == kClosing || state_ == kClosed)
return;
if (state_ == kConnecting) {
state_ = kClosing;
channel->Fail("WebSocket is closed before the connection is established.",
mojom::ConsoleMessageLevel::kWarning,
std::make_unique<SourceLocation>(String(), 0, 0, nullptr));
return;
}
state_ = kClosing;
if (channel)
channel->Close(code, cleansed_reason);
}
void WebSocketCommon::LogMixedAutoupgradeStatus(
blink::MixedContentAutoupgradeStatus status) const {
if (!was_autoupgraded_to_wss_)
return;
// For websockets we use the response received element to log successful
// connections.
UMA_HISTOGRAM_ENUMERATION("MixedAutoupgrade.Websocket.Status", status);
}
inline bool WebSocketCommon::IsValidSubprotocolCharacter(UChar character) {
const UChar kMinimumProtocolCharacter = '!'; // U+0021.
const UChar kMaximumProtocolCharacter = '~'; // U+007E.
// Set to true if character does not matches "separators" ABNF defined in
// RFC2616. SP and HT are excluded since the range check excludes them.
bool is_not_separator =
character != '"' && character != '(' && character != ')' &&
character != ',' && character != '/' &&
!(character >= ':' &&
character <=
'@') // U+003A - U+0040 (':', ';', '<', '=', '>', '?', '@').
&& !(character >= '[' &&
character <= ']') // U+005B - U+005D ('[', '\\', ']').
&& character != '{' && character != '}';
return character >= kMinimumProtocolCharacter &&
character <= kMaximumProtocolCharacter && is_not_separator;
}
bool WebSocketCommon::IsValidSubprotocolString(const String& protocol) {
if (protocol.IsEmpty())
return false;
for (wtf_size_t i = 0; i < protocol.length(); ++i) {
if (!IsValidSubprotocolCharacter(protocol[i]))
return false;
}
return true;
}
String WebSocketCommon::EncodeSubprotocolString(const String& protocol) {
StringBuilder builder;
for (wtf_size_t i = 0; i < protocol.length(); i++) {
if (protocol[i] < 0x20 || protocol[i] > 0x7E)
builder.AppendFormat("\\u%04X", protocol[i]);
else if (protocol[i] == 0x5c)
builder.Append("\\\\");
else
builder.Append(protocol[i]);
}
return builder.ToString();
}
String WebSocketCommon::JoinStrings(const Vector<String>& strings,
const char* separator) {
StringBuilder builder;
for (wtf_size_t i = 0; i < strings.size(); ++i) {
if (i)
builder.Append(separator);
builder.Append(strings[i]);
}
return builder.ToString();
}
} // namespace blink
// Copyright 2019 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.
// Common functionality shared between DOMWebSocket and WebSocketStream.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_COMMON_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_COMMON_H_
#include <memory>
#include "base/macros.h"
#include "third_party/blink/renderer/modules/modules_export.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "third_party/blink/renderer/platform/wtf/forward.h"
namespace blink {
class ExecutionContext;
class ExceptionState;
class WebSocketChannel;
enum class MixedContentAutoupgradeStatus;
// Implements connection- and closing- related functionality that otherwise
// would be duplicated between DOMWebSocket and WebSocketStream. Is embedded
// into those classes and delegated to when needed.
class MODULES_EXPORT WebSocketCommon {
DISALLOW_NEW();
public:
WebSocketCommon() = default;
~WebSocketCommon() = default;
enum State { kConnecting = 0, kOpen = 1, kClosing = 2, kClosed = 3 };
enum class ConnectResult { kSuccess, kException, kAsyncError };
// Checks |url| and |protocols| are valid, and starts a connection if they
// are.
ConnectResult Connect(ExecutionContext*,
const String& url,
const Vector<String>& protocols,
WebSocketChannel*,
ExceptionState&);
// Closes the connection if |code| and |reason| are valid.
void CloseInternal(int code,
const String& reason,
WebSocketChannel*,
ExceptionState&);
// Logs "MixedAutoupgrade.Websocket.Status" histogram. Does nothing unless
// |was_autoupgraded_to_wss_| is true.
void LogMixedAutoupgradeStatus(blink::MixedContentAutoupgradeStatus) const;
State GetState() const { return state_; }
void SetState(State state) { state_ = state; }
const KURL& Url() const { return url_; }
// The following methods are public for testing.
// Returns true if |protocol| is a valid WebSocket subprotocol name.
static bool IsValidSubprotocolString(const String& protocol);
// Escapes non-printing or non-ASCII characters in |protocol| as "\uXXXX" for
// inclusion in exception messages.
static String EncodeSubprotocolString(const String& protocol);
// Joins the strings in |strings| into a single string, with |separator|
// between each string.
static String JoinStrings(const Vector<String>& strings,
const char* separator);
private:
// Returns true if |character| is allowed in a WebSocket subprotocol name.
static bool IsValidSubprotocolCharacter(UChar character);
KURL url_;
bool was_autoupgraded_to_wss_ = false;
State state_ = kConnecting;
DISALLOW_COPY_AND_ASSIGN(WebSocketCommon);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_WEBSOCKETS_WEBSOCKET_COMMON_H_
// Copyright 2019 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 "third_party/blink/renderer/modules/websockets/websocket_common.h"
#include <string.h>
#include <algorithm>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
namespace {
// Connect() and CloseInternal() are very thoroughly tested by DOMWebSocket unit
// tests, so the rests aren't duplicated here.
// This test also indirectly tests IsValidSubprotocolCharacter.
TEST(WebSocketCommonTest, IsValidSubprotocolString) {
EXPECT_TRUE(WebSocketCommon::IsValidSubprotocolString("Helloworld!!"));
EXPECT_FALSE(WebSocketCommon::IsValidSubprotocolString("Hello, world!!"));
EXPECT_FALSE(WebSocketCommon::IsValidSubprotocolString(String()));
EXPECT_FALSE(WebSocketCommon::IsValidSubprotocolString(""));
const char kValidCharacters[] =
"!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`"
"abcdefghijklmnopqrstuvwxyz|~";
size_t length = strlen(kValidCharacters);
for (size_t i = 0; i < length; ++i) {
String s(kValidCharacters + i, 1u);
EXPECT_TRUE(WebSocketCommon::IsValidSubprotocolString(s));
}
for (size_t i = 0; i < 256; ++i) {
if (std::find(kValidCharacters, kValidCharacters + length,
static_cast<char>(i)) != kValidCharacters + length) {
continue;
}
char to_check = char{i};
String s(&to_check, 1u);
EXPECT_FALSE(WebSocketCommon::IsValidSubprotocolString(s));
}
}
TEST(WebSocketCommonTest, EncodeSubprotocolString) {
EXPECT_EQ("\\\\\\u0009\\u000D\\uFE0F ~hello\\u000A",
WebSocketCommon::EncodeSubprotocolString(u"\\\t\r\uFE0F ~hello\n"));
}
TEST(WebSocketCommonTest, JoinStrings) {
EXPECT_EQ("", WebSocketCommon::JoinStrings({}, ","));
EXPECT_EQ("ab", WebSocketCommon::JoinStrings({"ab"}, ","));
EXPECT_EQ("ab,c", WebSocketCommon::JoinStrings({"ab", "c"}, ","));
EXPECT_EQ("a\r\nbcd\r\nef",
WebSocketCommon::JoinStrings({"a", "bcd", "ef"}, "\r\n"));
EXPECT_EQ("|||", WebSocketCommon::JoinStrings({"|", "|"}, "|"));
// Non-ASCII strings are not required to work.
}
} // namespace
} // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment