blob: 94ce65baf8226cdf4723c259860606f1214d137c [file] [log] [blame]
// 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.
// Functions for transferable streams. See design doc
// https://docs.google.com/document/d/1_KuZzg5c3pncLJPFa8SuVm23AP4tft6mzPCL5at3I9M/edit
#include "third_party/blink/renderer/core/streams/transferable_streams.h"
#include "base/stl_util.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_post_message_options.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/events/message_event.h"
#include "third_party/blink/renderer/core/messaging/message_port.h"
#include "third_party/blink/renderer/core/streams/miscellaneous_operations.h"
#include "third_party/blink/renderer/core/streams/promise_handler.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/core/streams/readable_stream_default_controller.h"
#include "third_party/blink/renderer/core/streams/stream_algorithms.h"
#include "third_party/blink/renderer/core/streams/stream_promise_resolver.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/core/streams/writable_stream_default_controller.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/heap/visitor.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
#include "v8/include/v8.h"
// See the design doc at
// https://docs.google.com/document/d/1_KuZzg5c3pncLJPFa8SuVm23AP4tft6mzPCL5at3I9M/edit
// for explanation of how transferable streams are constructed from the "cross
// realm identity transform" implemented in this file.
// The peer (the other end of the MessagePort) is untrusted as it may be
// compromised. This means we have to be very careful in unpacking the messages
// from the peer. LOG(WARNING) is used for cases where a message from the peer
// appears to be invalid. If this appears during ordinary testing it indicates a
// bug.
//
// The -vmodule=transferable_streams=3 command-line argument can be used for
// debugging of the protocol.
namespace blink {
namespace {
// These are the types of messages that are sent between peers.
enum class MessageType { kPull, kCancel, kChunk, kClose, kAbort, kError };
// Creates a JavaScript object with a null prototype structured like {key1:
// value2, key2: value2}. This is used to create objects to be serialized by
// postMessage.
v8::Local<v8::Object> CreateKeyValueObject(v8::Isolate* isolate,
const char* key1,
v8::Local<v8::Value> value1,
const char* key2,
v8::Local<v8::Value> value2) {
v8::Local<v8::Name> names[] = {V8AtomicString(isolate, key1),
V8AtomicString(isolate, key2)};
v8::Local<v8::Value> values[] = {value1, value2};
static_assert(base::size(names) == base::size(values),
"names and values arrays must be the same size");
return v8::Object::New(isolate, v8::Null(isolate), names, values,
base::size(names));
}
// Unpacks an object created by CreateKeyValueObject(). |value1| and |value2|
// are out parameters. Returns false on failure.
bool UnpackKeyValueObject(ScriptState* script_state,
v8::Local<v8::Object> object,
const char* key1,
v8::Local<v8::Value>* value1,
const char* key2,
v8::Local<v8::Value>* value2) {
auto* isolate = script_state->GetIsolate();
v8::TryCatch try_catch(isolate);
auto context = script_state->GetContext();
if (!object->Get(context, V8AtomicString(isolate, key1)).ToLocal(value1)) {
DLOG(WARNING) << "Error reading key: '" << key1 << "'";
return false;
}
if (!object->Get(context, V8AtomicString(isolate, key2)).ToLocal(value2)) {
DLOG(WARNING) << "Error reading key: '" << key2 << "'";
return false;
}
return true;
}
// Sends a message with type |type| and contents |value| over |port|. The type
// is packed as a number with key "t", and the value is packed with key "v".
void PackAndPostMessage(ScriptState* script_state,
MessagePort* port,
MessageType type,
v8::Local<v8::Value> value,
ExceptionState& exception_state) {
DVLOG(3) << "PackAndPostMessage sending message type "
<< static_cast<int>(type);
auto* isolate = script_state->GetIsolate();
v8::Local<v8::Object> packed = CreateKeyValueObject(
isolate, "t", v8::Number::New(isolate, static_cast<int>(type)), "v",
value);
port->postMessage(script_state, ScriptValue(isolate, packed),
PostMessageOptions::Create(), exception_state);
}
// Sends a kError message to the remote side, disregarding failure.
void SendError(ScriptState* script_state,
MessagePort* port,
v8::Local<v8::Value> error) {
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", "");
PackAndPostMessage(script_state, port, MessageType::kError, error,
exception_state);
if (exception_state.HadException()) {
DLOG(WARNING) << "Disregarding exception while sending error";
exception_state.ClearException();
}
}
// Same as PackAndPostMessage(), except that it attempts to handle exceptions by
// sending a kError message to the remote side. On failure |error| is set to the
// original exception and the function returns false. Any error from sending the
// kError message is ignored.
bool PackAndPostMessageHandlingExceptions(ScriptState* script_state,
MessagePort* port,
MessageType type,
v8::Local<v8::Value> value,
v8::Local<v8::Value>* error) {
ExceptionState exception_state(script_state->GetIsolate(),
ExceptionState::kUnknownContext, "", "");
PackAndPostMessage(script_state, port, type, value, exception_state);
if (exception_state.HadException()) {
*error = exception_state.GetException();
SendError(script_state, port, *error);
exception_state.ClearException();
return false;
}
return true;
}
// Base class for CrossRealmTransformWritable and CrossRealmTransformReadable.
// Contains common methods that are used when handling MessagePort events.
class CrossRealmTransformStream
: public GarbageCollected<CrossRealmTransformStream> {
public:
// Neither of the subclasses require finalization, so no destructor.
virtual ScriptState* GetScriptState() const = 0;
virtual MessagePort* GetMessagePort() const = 0;
// HandleMessage() is called by CrossRealmTransformMessageListener to handle
// an incoming message from the MessagePort.
virtual void HandleMessage(MessageType type, v8::Local<v8::Value> value) = 0;
// HandleError() is called by CrossRealmTransformErrorListener when an error
// event is fired on the message port. It should error the stream.
virtual void HandleError(v8::Local<v8::Value> error) = 0;
virtual void Trace(Visitor*) const {}
};
// Handles MessageEvents from the MessagePort.
class CrossRealmTransformMessageListener final : public NativeEventListener {
public:
explicit CrossRealmTransformMessageListener(CrossRealmTransformStream* target)
: target_(target) {}
void Invoke(ExecutionContext*, Event* event) override {
// TODO(ricea): Find a way to guarantee this cast is safe.
MessageEvent* message = static_cast<MessageEvent*>(event);
ScriptState* script_state = target_->GetScriptState();
// The deserializer code called by message->data() looks up the ScriptState
// from the current context, so we need to make sure it is set.
ScriptState::Scope scope(script_state);
v8::Local<v8::Value> data = message->data(script_state).V8Value();
if (!data->IsObject()) {
DLOG(WARNING) << "Invalid message from peer ignored (not object)";
return;
}
v8::Local<v8::Value> type;
v8::Local<v8::Value> value;
if (!UnpackKeyValueObject(script_state, data.As<v8::Object>(), "t", &type,
"v", &value)) {
DLOG(WARNING) << "Invalid message from peer ignored";
return;
}
if (!type->IsNumber()) {
DLOG(WARNING) << "Invalid message from peer ignored (type is not number)";
return;
}
int type_value = type.As<v8::Number>()->Value();
DVLOG(3) << "MessageListener saw message type " << type_value;
target_->HandleMessage(static_cast<MessageType>(type_value), value);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(target_);
NativeEventListener::Trace(visitor);
}
private:
const Member<CrossRealmTransformStream> target_;
};
// Handles "error" events from the MessagePort.
class CrossRealmTransformErrorListener final : public NativeEventListener {
public:
explicit CrossRealmTransformErrorListener(CrossRealmTransformStream* target)
: target_(target) {}
void Invoke(ExecutionContext*, Event*) override {
ScriptState* script_state = target_->GetScriptState();
const auto* error =
DOMException::Create("chunk could not be cloned", "DataCloneError");
auto* message_port = target_->GetMessagePort();
v8::Local<v8::Value> error_value = ToV8(error, script_state);
SendError(script_state, message_port, error_value);
message_port->close();
target_->HandleError(error_value);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(target_);
NativeEventListener::Trace(visitor);
}
private:
const Member<CrossRealmTransformStream> target_;
};
// Class for data associated with the writable side of the cross realm transform
// stream.
class CrossRealmTransformWritable final : public CrossRealmTransformStream {
public:
CrossRealmTransformWritable(ScriptState* script_state, MessagePort* port)
: script_state_(script_state),
message_port_(port),
backpressure_promise_(
MakeGarbageCollected<StreamPromiseResolver>(script_state)) {}
WritableStream* CreateWritableStream(ExceptionState&);
ScriptState* GetScriptState() const override { return script_state_; }
MessagePort* GetMessagePort() const override { return message_port_; }
void HandleMessage(MessageType type, v8::Local<v8::Value> value) override;
void HandleError(v8::Local<v8::Value> error) override;
void Trace(Visitor* visitor) const override {
visitor->Trace(script_state_);
visitor->Trace(message_port_);
visitor->Trace(backpressure_promise_);
visitor->Trace(controller_);
CrossRealmTransformStream::Trace(visitor);
}
private:
class WriteAlgorithm;
class CloseAlgorithm;
class AbortAlgorithm;
const Member<ScriptState> script_state_;
const Member<MessagePort> message_port_;
Member<StreamPromiseResolver> backpressure_promise_;
Member<WritableStreamDefaultController> controller_;
};
class CrossRealmTransformWritable::WriteAlgorithm final
: public StreamAlgorithm {
public:
explicit WriteAlgorithm(CrossRealmTransformWritable* writable)
: writable_(writable) {}
// Sends the chunk to the readable side, possibly after waiting for
// backpressure.
v8::Local<v8::Promise> Run(ScriptState* script_state,
int argc,
v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 1);
auto chunk = argv[0];
if (!writable_->backpressure_promise_) {
return DoWrite(script_state, chunk);
}
auto* isolate = script_state->GetIsolate();
return StreamThenPromise(
script_state->GetContext(),
writable_->backpressure_promise_->V8Promise(isolate),
MakeGarbageCollected<DoWriteOnResolve>(script_state, chunk, this));
}
void Trace(Visitor* visitor) const override {
visitor->Trace(writable_);
StreamAlgorithm::Trace(visitor);
}
private:
// A promise handler which calls DoWrite() when the promise resolves.
class DoWriteOnResolve final : public PromiseHandlerWithValue {
public:
DoWriteOnResolve(ScriptState* script_state,
v8::Local<v8::Value> chunk,
WriteAlgorithm* target)
: PromiseHandlerWithValue(script_state),
chunk_(script_state->GetIsolate(), chunk),
target_(target) {}
v8::Local<v8::Value> CallWithLocal(v8::Local<v8::Value>) override {
ScriptState* script_state = GetScriptState();
return target_->DoWrite(script_state,
chunk_.NewLocal(script_state->GetIsolate()));
}
void Trace(Visitor* visitor) const override {
visitor->Trace(chunk_);
visitor->Trace(target_);
PromiseHandlerWithValue::Trace(visitor);
}
private:
const TraceWrapperV8Reference<v8::Value> chunk_;
const Member<WriteAlgorithm> target_;
};
// Sends a chunk over the message port to the readable side.
v8::Local<v8::Promise> DoWrite(ScriptState* script_state,
v8::Local<v8::Value> chunk) {
writable_->backpressure_promise_ =
MakeGarbageCollected<StreamPromiseResolver>(script_state);
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kChunk, chunk,
&error);
if (!success) {
writable_->message_port_->close();
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state);
}
const Member<CrossRealmTransformWritable> writable_;
};
class CrossRealmTransformWritable::CloseAlgorithm final
: public StreamAlgorithm {
public:
explicit CloseAlgorithm(CrossRealmTransformWritable* writable)
: writable_(writable) {}
// Sends a close message to the readable side and closes the message port.
v8::Local<v8::Promise> Run(ScriptState* script_state,
int argc,
v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 0);
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kClose,
v8::Undefined(script_state->GetIsolate()), &error);
writable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(writable_);
StreamAlgorithm::Trace(visitor);
}
private:
const Member<CrossRealmTransformWritable> writable_;
};
class CrossRealmTransformWritable::AbortAlgorithm final
: public StreamAlgorithm {
public:
explicit AbortAlgorithm(CrossRealmTransformWritable* writable)
: writable_(writable) {}
// Sends an abort message to the readable side and closes the message port.
v8::Local<v8::Promise> Run(ScriptState* script_state,
int argc,
v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 1);
auto reason = argv[0];
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, writable_->message_port_, MessageType::kAbort, reason,
&error);
writable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(writable_);
StreamAlgorithm::Trace(visitor);
}
private:
const Member<CrossRealmTransformWritable> writable_;
};
WritableStream* CrossRealmTransformWritable::CreateWritableStream(
ExceptionState& exception_state) {
DCHECK(!controller_) << "CreateWritableStream() can only be called once";
message_port_->setOnmessage(
MakeGarbageCollected<CrossRealmTransformMessageListener>(this));
message_port_->setOnmessageerror(
MakeGarbageCollected<CrossRealmTransformErrorListener>(this));
auto* stream =
WritableStream::Create(script_state_, CreateTrivialStartAlgorithm(),
MakeGarbageCollected<WriteAlgorithm>(this),
MakeGarbageCollected<CloseAlgorithm>(this),
MakeGarbageCollected<AbortAlgorithm>(this), 1,
CreateDefaultSizeAlgorithm(), exception_state);
if (exception_state.HadException()) {
return nullptr;
}
controller_ = stream->Controller();
return stream;
}
void CrossRealmTransformWritable::HandleMessage(MessageType type,
v8::Local<v8::Value> value) {
switch (type) {
case MessageType::kPull:
DCHECK(backpressure_promise_);
backpressure_promise_->ResolveWithUndefined(script_state_);
backpressure_promise_ = nullptr;
return;
case MessageType::kCancel:
case MessageType::kError: {
WritableStreamDefaultController::ErrorIfNeeded(script_state_, controller_,
value);
if (backpressure_promise_) {
backpressure_promise_->ResolveWithUndefined(script_state_);
backpressure_promise_ = nullptr;
}
return;
}
default:
DLOG(WARNING) << "Invalid message from peer ignored (invalid type): "
<< static_cast<int>(type);
return;
}
}
void CrossRealmTransformWritable::HandleError(v8::Local<v8::Value> error) {
WritableStreamDefaultController::ErrorIfNeeded(script_state_, controller_,
error);
}
// Class for data associated with the readable side of the cross realm transform
// stream.
class CrossRealmTransformReadable final : public CrossRealmTransformStream {
public:
CrossRealmTransformReadable(ScriptState* script_state, MessagePort* port)
: script_state_(script_state), message_port_(port) {}
ReadableStream* CreateReadableStream(ExceptionState&);
ScriptState* GetScriptState() const override { return script_state_; }
MessagePort* GetMessagePort() const override { return message_port_; }
void HandleMessage(MessageType type, v8::Local<v8::Value> value) override;
void HandleError(v8::Local<v8::Value> error) override;
void Trace(Visitor* visitor) const override {
visitor->Trace(script_state_);
visitor->Trace(message_port_);
visitor->Trace(controller_);
CrossRealmTransformStream::Trace(visitor);
}
private:
class PullAlgorithm;
class CancelAlgorithm;
const Member<ScriptState> script_state_;
const Member<MessagePort> message_port_;
Member<ReadableStreamDefaultController> controller_;
bool finished_ = false;
};
class CrossRealmTransformReadable::PullAlgorithm final
: public StreamAlgorithm {
public:
explicit PullAlgorithm(CrossRealmTransformReadable* readable)
: readable_(readable) {}
// Sends a pull message to the writable side and then waits for backpressure
// to clear.
v8::Local<v8::Promise> Run(ScriptState* script_state,
int argc,
v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 0);
auto* isolate = script_state->GetIsolate();
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, readable_->message_port_, MessageType::kPull,
v8::Undefined(isolate), &error);
if (!success) {
readable_->message_port_->close();
return PromiseReject(script_state, error);
}
// The Streams Standard guarantees that PullAlgorithm won't be called again
// until Enqueue() is called.
return PromiseResolveWithUndefined(script_state);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(readable_);
StreamAlgorithm::Trace(visitor);
}
private:
const Member<CrossRealmTransformReadable> readable_;
};
class CrossRealmTransformReadable::CancelAlgorithm final
: public StreamAlgorithm {
public:
explicit CancelAlgorithm(CrossRealmTransformReadable* readable)
: readable_(readable) {}
// Sends a cancel message to the writable side and closes the message port.
v8::Local<v8::Promise> Run(ScriptState* script_state,
int argc,
v8::Local<v8::Value> argv[]) override {
DCHECK_EQ(argc, 1);
auto reason = argv[0];
readable_->finished_ = true;
v8::Local<v8::Value> error;
bool success = PackAndPostMessageHandlingExceptions(
script_state, readable_->message_port_, MessageType::kCancel, reason,
&error);
readable_->message_port_->close();
if (!success) {
return PromiseReject(script_state, error);
}
return PromiseResolveWithUndefined(script_state);
}
void Trace(Visitor* visitor) const override {
visitor->Trace(readable_);
StreamAlgorithm::Trace(visitor);
}
private:
const Member<CrossRealmTransformReadable> readable_;
};
ReadableStream* CrossRealmTransformReadable::CreateReadableStream(
ExceptionState& exception_state) {
DCHECK(!controller_) << "CreateReadableStream can only be called once";
message_port_->setOnmessage(
MakeGarbageCollected<CrossRealmTransformMessageListener>(this));
message_port_->setOnmessageerror(
MakeGarbageCollected<CrossRealmTransformErrorListener>(this));
auto* stream = ReadableStream::Create(
script_state_, CreateTrivialStartAlgorithm(),
MakeGarbageCollected<PullAlgorithm>(this),
MakeGarbageCollected<CancelAlgorithm>(this),
/* highWaterMark = */ 0, CreateDefaultSizeAlgorithm(), exception_state);
if (exception_state.HadException()) {
return nullptr;
}
controller_ = stream->GetController();
return stream;
}
void CrossRealmTransformReadable::HandleMessage(MessageType type,
v8::Local<v8::Value> value) {
switch (type) {
case MessageType::kChunk: {
if (ReadableStreamDefaultController::CanCloseOrEnqueue(controller_)) {
// This can't throw because we always use the default strategy size
// algorithm, which doesn't throw, and always returns a valid value of
// 1.0.
ReadableStreamDefaultController::Enqueue(script_state_, controller_,
value, ASSERT_NO_EXCEPTION);
}
return;
}
case MessageType::kClose:
finished_ = true;
if (ReadableStreamDefaultController::CanCloseOrEnqueue(controller_)) {
ReadableStreamDefaultController::Close(script_state_, controller_);
}
message_port_->close();
return;
case MessageType::kAbort:
case MessageType::kError: {
finished_ = true;
ReadableStreamDefaultController::Error(script_state_, controller_, value);
message_port_->close();
return;
}
default:
DLOG(WARNING) << "Invalid message from peer ignored (invalid type): "
<< static_cast<int>(type);
return;
}
}
void CrossRealmTransformReadable::HandleError(v8::Local<v8::Value> error) {
ReadableStreamDefaultController::Error(script_state_, controller_, error);
}
} // namespace
CORE_EXPORT WritableStream* CreateCrossRealmTransformWritable(
ScriptState* script_state,
MessagePort* port,
ExceptionState& exception_state) {
return MakeGarbageCollected<CrossRealmTransformWritable>(script_state, port)
->CreateWritableStream(exception_state);
}
CORE_EXPORT ReadableStream* CreateCrossRealmTransformReadable(
ScriptState* script_state,
MessagePort* port,
ExceptionState& exception_state) {
return MakeGarbageCollected<CrossRealmTransformReadable>(script_state, port)
->CreateReadableStream(exception_state);
}
} // namespace blink