blob: cca1abca9a305686dc216b013a14099862a6fb70 [file] [log] [blame]
// Copyright 2020 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/webtransport/bidirectional_stream.h"
#include <memory>
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/notreached.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/mojom/quic_transport.mojom-blink.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/webtransport/quic_transport_connector.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_iterator_result_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_uint8_array.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_bidirectional_stream.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_quic_transport_options.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/core/streams/readable_stream_default_reader.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/core/streams/writable_stream_default_writer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h"
#include "third_party/blink/renderer/modules/webtransport/quic_transport.h"
#include "third_party/blink/renderer/modules/webtransport/test_utils.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
// These tests only ever create one stream at a time, so use a hardcoded stream
// id.
constexpr uint32_t kDefaultStreamId = 0;
// BidirectionalStream depends on blink::QuicTransport. Rather than virtualise
// blink::QuicTransport for these tests, we use a stub implementation of
// network::mojom::blink::QuicTransport to get the behaviour we want. This class
// only supports the creation of one BidirectionalStream at a time for
// simplicity.
class StubQuicTransport : public network::mojom::blink::QuicTransport {
public:
explicit StubQuicTransport(
mojo::PendingReceiver<network::mojom::blink::QuicTransport>
pending_receiver)
: receiver_(this, std::move(pending_receiver)) {}
// Functions used by tests to inspect and manipulate the object.
// Data written to the |writable| side of the bidirectional stream can be read
// from this handle.
mojo::ScopedDataPipeConsumerHandle& OutputConsumer() {
return output_consumer_;
}
// Data written to this handle will appear on the |readable| side of the
// bidirectional stream.
mojo::ScopedDataPipeProducerHandle& InputProducer() {
return input_producer_;
}
bool WasSendFinCalled() const { return was_send_fin_called_; }
bool WasAbortStreamCalled() const { return was_abort_stream_called_; }
// Responds to an earlier call to AcceptBidirectionalStream with a new stream
// as if it was created by the remote server. The remote handles can be
// accessed via OutputConsumer() and InputConsumer() as with locally-created
// streams.
void CreateRemote() {
ASSERT_TRUE(accept_callback_);
mojo::ScopedDataPipeProducerHandle output_producer;
mojo::ScopedDataPipeConsumerHandle output_consumer;
ASSERT_TRUE(
CreateDataPipeForWebTransportTests(&output_producer, &output_consumer));
output_consumer_ = std::move(output_consumer);
mojo::ScopedDataPipeProducerHandle input_producer;
mojo::ScopedDataPipeConsumerHandle input_consumer;
ASSERT_TRUE(
CreateDataPipeForWebTransportTests(&input_producer, &input_consumer));
input_producer_ = std::move(input_producer);
std::move(accept_callback_)
.Run(kDefaultStreamId, std::move(input_consumer),
std::move(output_producer));
// This prevents redundant calls to AcceptBidirectionalStream() by ensuring
// the call to Enqueue() happens before the next call to pull().
test::RunPendingTasks();
}
// Implementation of QuicTransport.
void SendDatagram(base::span<const uint8_t> data,
base::OnceCallback<void(bool)>) override {
NOTREACHED();
}
void CreateStream(
mojo::ScopedDataPipeConsumerHandle output_consumer,
mojo::ScopedDataPipeProducerHandle input_producer,
base::OnceCallback<void(bool, uint32_t)> callback) override {
EXPECT_TRUE(output_consumer.is_valid());
EXPECT_FALSE(output_consumer_.is_valid());
output_consumer_ = std::move(output_consumer);
EXPECT_TRUE(input_producer.is_valid());
EXPECT_FALSE(input_producer_.is_valid());
input_producer_ = std::move(input_producer);
std::move(callback).Run(true, kDefaultStreamId);
}
void AcceptBidirectionalStream(
base::OnceCallback<void(uint32_t,
mojo::ScopedDataPipeConsumerHandle,
mojo::ScopedDataPipeProducerHandle)> callback)
override {
DCHECK(!accept_callback_);
accept_callback_ = std::move(callback);
}
void AcceptUnidirectionalStream(
base::OnceCallback<void(uint32_t, mojo::ScopedDataPipeConsumerHandle)>
callback) override {
DCHECK(!ignored_unidirectional_stream_callback_);
// This method is always called. We have to retain the callback to avoid an
// error about early destruction, but never call it.
ignored_unidirectional_stream_callback_ = std::move(callback);
}
void SendFin(uint32_t stream_id) override {
EXPECT_EQ(stream_id, kDefaultStreamId);
was_send_fin_called_ = true;
}
void AbortStream(uint32_t stream_id, uint64_t code) override {
EXPECT_EQ(stream_id, kDefaultStreamId);
was_abort_stream_called_ = true;
}
void SetOutgoingDatagramExpirationDuration(base::TimeDelta) override {}
private:
base::OnceCallback<void(uint32_t,
mojo::ScopedDataPipeConsumerHandle,
mojo::ScopedDataPipeProducerHandle)>
accept_callback_;
base::OnceCallback<void(uint32_t, mojo::ScopedDataPipeConsumerHandle)>
ignored_unidirectional_stream_callback_;
mojo::Receiver<network::mojom::blink::QuicTransport> receiver_;
mojo::ScopedDataPipeConsumerHandle output_consumer_;
mojo::ScopedDataPipeProducerHandle input_producer_;
bool was_send_fin_called_ = false;
bool was_abort_stream_called_ = false;
};
// This class sets up a connected blink::QuicTransport object using a
// StubQuicTransport and provides access to both.
class ScopedQuicTransport : public mojom::blink::QuicTransportConnector {
STACK_ALLOCATED();
public:
// For convenience, this class does work in the constructor. This is okay
// because it is only used for testing.
explicit ScopedQuicTransport(const V8TestingScope& scope)
: browser_interface_broker_(
&scope.GetExecutionContext()->GetBrowserInterfaceBroker()) {
browser_interface_broker_->SetBinderForTesting(
mojom::blink::QuicTransportConnector::Name_,
base::BindRepeating(&ScopedQuicTransport::BindConnector,
weak_ptr_factory_.GetWeakPtr()));
quic_transport_ = QuicTransport::Create(
scope.GetScriptState(), "quic-transport://example.com/",
MakeGarbageCollected<QuicTransportOptions>(), ASSERT_NO_EXCEPTION);
test::RunPendingTasks();
}
~ScopedQuicTransport() override {
browser_interface_broker_->SetBinderForTesting(
mojom::blink::QuicTransportConnector::Name_, {});
}
QuicTransport* GetQuicTransport() const { return quic_transport_; }
StubQuicTransport* Stub() const { return stub_.get(); }
void ResetStub() { stub_.reset(); }
BidirectionalStream* CreateBidirectionalStream(const V8TestingScope& scope) {
auto* script_state = scope.GetScriptState();
ScriptPromise bidirectional_stream_promise =
quic_transport_->createBidirectionalStream(script_state,
ASSERT_NO_EXCEPTION);
ScriptPromiseTester tester(script_state, bidirectional_stream_promise);
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
auto* bidirectional_stream = V8BidirectionalStream::ToImplWithTypeCheck(
scope.GetIsolate(), tester.Value().V8Value());
EXPECT_TRUE(bidirectional_stream);
return bidirectional_stream;
}
BidirectionalStream* RemoteCreateBidirectionalStream(
const V8TestingScope& scope) {
stub_->CreateRemote();
ReadableStream* streams = quic_transport_->receiveBidirectionalStreams();
v8::Local<v8::Value> v8value = ReadValueFromStream(scope, streams);
BidirectionalStream* bidirectional_stream =
V8BidirectionalStream::ToImplWithTypeCheck(scope.GetIsolate(), v8value);
EXPECT_TRUE(bidirectional_stream);
return bidirectional_stream;
}
// Implementation of mojom::blink::QuicTransportConnector.
void Connect(
const KURL&,
Vector<network::mojom::blink::QuicTransportCertificateFingerprintPtr>
fingerprints,
mojo::PendingRemote<network::mojom::blink::QuicTransportHandshakeClient>
pending_handshake_client) override {
mojo::Remote<network::mojom::blink::QuicTransportHandshakeClient>
handshake_client(std::move(pending_handshake_client));
mojo::PendingRemote<network::mojom::blink::QuicTransport>
quic_transport_to_pass;
mojo::PendingRemote<network::mojom::blink::QuicTransportClient>
client_remote;
stub_ = std::make_unique<StubQuicTransport>(
quic_transport_to_pass.InitWithNewPipeAndPassReceiver());
handshake_client->OnConnectionEstablished(
std::move(quic_transport_to_pass),
client_remote.InitWithNewPipeAndPassReceiver());
client_remote_.Bind(std::move(client_remote));
}
private:
void BindConnector(mojo::ScopedMessagePipeHandle handle) {
connector_receiver_.Bind(
mojo::PendingReceiver<mojom::blink::QuicTransportConnector>(
std::move(handle)));
}
// |browser_interface_broker_| is cached here because we need to use it in the
// destructor. This means ScopedQuicTransport must always be destroyed before
// the V8TestingScope object that owns the BrowserInterfaceBrokerProxy.
const BrowserInterfaceBrokerProxy* browser_interface_broker_;
QuicTransport* quic_transport_;
std::unique_ptr<StubQuicTransport> stub_;
mojo::Remote<network::mojom::blink::QuicTransportClient> client_remote_;
mojo::Receiver<mojom::blink::QuicTransportConnector> connector_receiver_{
this};
base::WeakPtrFactory<ScopedQuicTransport> weak_ptr_factory_{this};
};
// This test fragment is common to CreateLocallyAndWrite and
// CreateRemotelyAndWrite.
void TestWrite(const V8TestingScope& scope,
ScopedQuicTransport* scoped_quic_transport,
BidirectionalStream* bidirectional_stream) {
auto* script_state = scope.GetScriptState();
auto* writer = bidirectional_stream->writable()->getWriter(
script_state, ASSERT_NO_EXCEPTION);
auto* chunk = DOMUint8Array::Create(1);
*chunk->Data() = 'A';
ScriptPromise result =
writer->write(script_state, ScriptValue::From(script_state, chunk),
ASSERT_NO_EXCEPTION);
ScriptPromiseTester tester(script_state, result);
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
EXPECT_TRUE(tester.Value().IsUndefined());
mojo::ScopedDataPipeConsumerHandle& output_consumer =
scoped_quic_transport->Stub()->OutputConsumer();
const void* buffer = nullptr;
uint32_t buffer_num_bytes = 0;
MojoResult mojo_result = output_consumer->BeginReadData(
&buffer, &buffer_num_bytes, MOJO_BEGIN_READ_DATA_FLAG_NONE);
ASSERT_EQ(mojo_result, MOJO_RESULT_OK);
EXPECT_EQ(buffer_num_bytes, 1u);
EXPECT_EQ(reinterpret_cast<const char*>(buffer)[0], 'A');
output_consumer->EndReadData(buffer_num_bytes);
}
TEST(BidirectionalStreamTest, CreateLocallyAndWrite) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
TestWrite(scope, &scoped_quic_transport, bidirectional_stream);
}
TEST(BidirectionalStreamTest, CreateRemotelyAndWrite) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.RemoteCreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
TestWrite(scope, &scoped_quic_transport, bidirectional_stream);
}
// This test fragment is common to CreateLocallyAndRead and
// CreateRemotelyAndRead.
void TestRead(const V8TestingScope& scope,
ScopedQuicTransport* scoped_quic_transport,
BidirectionalStream* bidirectional_stream) {
mojo::ScopedDataPipeProducerHandle& input_producer =
scoped_quic_transport->Stub()->InputProducer();
constexpr char input[] = {'B'};
uint32_t input_num_bytes = sizeof(input);
MojoResult mojo_result = input_producer->WriteData(
input, &input_num_bytes, MOJO_WRITE_DATA_FLAG_ALL_OR_NONE);
ASSERT_EQ(mojo_result, MOJO_RESULT_OK);
v8::Local<v8::Value> v8array =
ReadValueFromStream(scope, bidirectional_stream->readable());
DOMUint8Array* u8array =
V8Uint8Array::ToImplWithTypeCheck(scope.GetIsolate(), v8array);
ASSERT_TRUE(u8array);
ASSERT_EQ(u8array->byteLength(), 1u);
EXPECT_EQ(reinterpret_cast<char*>(u8array->Data())[0], 'B');
}
TEST(BidirectionalStreamTest, CreateLocallyAndRead) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
TestRead(scope, &scoped_quic_transport, bidirectional_stream);
}
TEST(BidirectionalStreamTest, CreateRemotelyAndRead) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.RemoteCreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
TestRead(scope, &scoped_quic_transport, bidirectional_stream);
}
TEST(BidirectionalStreamTest, IncomingStreamCleanClose) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
scoped_quic_transport.GetQuicTransport()->OnIncomingStreamClosed(
kDefaultStreamId, true);
scoped_quic_transport.Stub()->InputProducer().reset();
auto* script_state = scope.GetScriptState();
auto* reader = bidirectional_stream->readable()->GetDefaultReaderForTesting(
script_state, ASSERT_NO_EXCEPTION);
ScriptPromise read_promise = reader->read(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester read_tester(script_state, read_promise);
read_tester.WaitUntilSettled();
EXPECT_TRUE(read_tester.IsFulfilled());
v8::Local<v8::Value> result = read_tester.Value().V8Value();
DCHECK(result->IsObject());
v8::Local<v8::Value> v8value;
bool done = false;
EXPECT_TRUE(
V8UnpackIteratorResult(script_state, result.As<v8::Object>(), &done)
.ToLocal(&v8value));
EXPECT_TRUE(done);
ScriptPromiseTester tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, IncomingStreamAbort) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
bidirectional_stream->abortReading(nullptr);
ScriptPromiseTester tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, OutgoingStreamAbort) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
bidirectional_stream->abortWriting(nullptr);
ScriptPromiseTester tester(scope.GetScriptState(),
bidirectional_stream->readingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
const auto* const stub = scoped_quic_transport.Stub();
EXPECT_FALSE(stub->WasSendFinCalled());
EXPECT_TRUE(stub->WasAbortStreamCalled());
}
TEST(BidirectionalStreamTest, OutgoingStreamCleanClose) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
auto* script_state = scope.GetScriptState();
ScriptPromise close_promise = bidirectional_stream->writable()->close(
script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester close_tester(script_state, close_promise);
close_tester.WaitUntilSettled();
EXPECT_TRUE(close_tester.IsFulfilled());
// The incoming side is closed by the network service.
scoped_quic_transport.GetQuicTransport()->OnIncomingStreamClosed(
kDefaultStreamId, false);
scoped_quic_transport.Stub()->InputProducer().reset();
ScriptPromiseTester tester(script_state,
bidirectional_stream->readingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
const auto* const stub = scoped_quic_transport.Stub();
EXPECT_TRUE(stub->WasSendFinCalled());
EXPECT_FALSE(stub->WasAbortStreamCalled());
}
TEST(BidirectionalStreamTest, AbortBothOutgoingFirst) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
bidirectional_stream->abortWriting(nullptr);
bidirectional_stream->abortReading(nullptr);
ScriptPromiseTester reading_tester(scope.GetScriptState(),
bidirectional_stream->readingAborted());
reading_tester.WaitUntilSettled();
EXPECT_TRUE(reading_tester.IsFulfilled());
ScriptPromiseTester writing_tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
writing_tester.WaitUntilSettled();
EXPECT_TRUE(writing_tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, AbortBothIncomingFirst) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
bidirectional_stream->abortReading(nullptr);
bidirectional_stream->abortWriting(nullptr);
ScriptPromiseTester reading_tester(scope.GetScriptState(),
bidirectional_stream->readingAborted());
reading_tester.WaitUntilSettled();
EXPECT_TRUE(reading_tester.IsFulfilled());
ScriptPromiseTester writing_tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
writing_tester.WaitUntilSettled();
EXPECT_TRUE(writing_tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, CloseOutgoingThenAbortIncoming) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
// 1. Close outgoing.
auto* script_state = scope.GetScriptState();
ScriptPromise close_promise = bidirectional_stream->writable()->close(
script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester close_tester(script_state, close_promise);
close_tester.WaitUntilSettled();
EXPECT_TRUE(close_tester.IsFulfilled());
// 2. Abort incoming.
bidirectional_stream->abortReading(nullptr);
// 3. The network service closes the incoming data pipe as a result of 1.
scoped_quic_transport.GetQuicTransport()->OnIncomingStreamClosed(
kDefaultStreamId, false);
scoped_quic_transport.Stub()->InputProducer().reset();
ScriptPromiseTester tester(script_state,
bidirectional_stream->readingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, AbortIncomingThenCloseOutgoing) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
// 1. Abort incoming.
bidirectional_stream->abortReading(nullptr);
// 2. Close outgoing. It should already have been closed when we aborted
// reading, so this should be a no-op.
auto* script_state = scope.GetScriptState();
ScriptPromise close_promise = bidirectional_stream->writable()->close(
script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester close_tester(script_state, close_promise);
close_tester.WaitUntilSettled();
EXPECT_TRUE(close_tester.IsRejected());
ScriptPromiseTester tester(script_state,
bidirectional_stream->writingAborted());
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, CloseQuicTransport) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
scoped_quic_transport.GetQuicTransport()->close(nullptr);
ScriptPromiseTester reading_tester(scope.GetScriptState(),
bidirectional_stream->readingAborted());
reading_tester.WaitUntilSettled();
EXPECT_TRUE(reading_tester.IsFulfilled());
ScriptPromiseTester writing_tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
writing_tester.WaitUntilSettled();
EXPECT_TRUE(writing_tester.IsFulfilled());
}
TEST(BidirectionalStreamTest, RemoteDropQuicTransport) {
V8TestingScope scope;
ScopedQuicTransport scoped_quic_transport(scope);
auto* bidirectional_stream =
scoped_quic_transport.CreateBidirectionalStream(scope);
ASSERT_TRUE(bidirectional_stream);
scoped_quic_transport.ResetStub();
ScriptPromiseTester reading_tester(scope.GetScriptState(),
bidirectional_stream->readingAborted());
reading_tester.WaitUntilSettled();
EXPECT_TRUE(reading_tester.IsFulfilled());
ScriptPromiseTester writing_tester(scope.GetScriptState(),
bidirectional_stream->writingAborted());
writing_tester.WaitUntilSettled();
EXPECT_TRUE(writing_tester.IsFulfilled());
}
} // namespace
} // namespace blink