Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#include "WebTransport.h"
#include "WebTransportBidirectionalStream.h"
#include "mozilla/RefPtr.h"
#include "nsUTF8Utils.h"
#include "nsIURL.h"
#include "nsIWebTransportStream.h"
#include "mozilla/Assertions.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/DOMExceptionBinding.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PWebTransport.h"
#include "mozilla/dom/ReadableStream.h"
#include "mozilla/dom/ReadableStreamDefaultController.h"
#include "mozilla/dom/RemoteWorkerChild.h"
#include "mozilla/dom/WebTransportDatagramDuplexStream.h"
#include "mozilla/dom/WebTransportError.h"
#include "mozilla/dom/WebTransportLog.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WritableStream.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/Endpoint.h"
#include "mozilla/ipc/PBackgroundChild.h"
using namespace mozilla::ipc;
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(WebTransport)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WebTransport)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingUnidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingBidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingUnidirectionalAlgorithm)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingBidirectionalAlgorithm)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDatagrams)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReady)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClosed)
for (const auto& hashEntry : tmp->mSendStreams.Values()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mSendStreams entry item");
cb.NoteXPCOMChild(hashEntry);
}
for (const auto& hashEntry : tmp->mReceiveStreams.Values()) {
NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mReceiveStreams entry item");
cb.NoteXPCOMChild(hashEntry);
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WebTransport)
tmp->mSendStreams.Clear();
tmp->mReceiveStreams.Clear();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mBidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingUnidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingBidirectionalStreams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingUnidirectionalAlgorithm)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingBidirectionalAlgorithm)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDatagrams)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mReady)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mClosed)
if (tmp->mChild) {
tmp->mChild->Shutdown(false);
tmp->mChild = nullptr;
}
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(WebTransport)
NS_IMPL_CYCLE_COLLECTING_RELEASE(WebTransport)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebTransport)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
WebTransport::WebTransport(nsIGlobalObject* aGlobal)
: mGlobal(aGlobal),
mState(WebTransportState::CONNECTING),
mReliability(WebTransportReliabilityMode::Pending) {
LOG(("Creating WebTransport %p", this));
}
WebTransport::~WebTransport() {
// Should be empty by this point, because we should always have run cleanup:
LOG(("~WebTransport() for %p", this));
MOZ_ASSERT(mSendStreams.IsEmpty());
MOZ_ASSERT(mReceiveStreams.IsEmpty());
// If this WebTransport was destroyed without being closed properly, make
// sure to clean up the channel.
// Since child has a raw ptr to us, we MUST call Shutdown() before we're
// destroyed
if (mChild) {
mChild->Shutdown(true);
}
}
// From parent
void WebTransport::NewBidirectionalStream(
uint64_t aStreamId, const RefPtr<DataPipeReceiver>& aIncoming,
const RefPtr<DataPipeSender>& aOutgoing) {
LOG_VERBOSE(("NewBidirectionalStream()"));
// Create a Bidirectional stream and push it into the
// IncomingBidirectionalStreams stream. Must be added to the ReceiveStreams
// and SendStreams arrays
UniquePtr<BidirectionalPair> streams(
new BidirectionalPair(aIncoming, aOutgoing));
auto tuple = std::tuple<uint64_t, UniquePtr<BidirectionalPair>>(
aStreamId, std::move(streams));
mBidirectionalStreams.AppendElement(std::move(tuple));
// We need to delete them all!
// Notify something to wake up readers of IncomingReceiveStreams
// The callback is always set/used from the same thread (MainThread or a
// Worker thread).
if (mIncomingBidirectionalAlgorithm) {
RefPtr<WebTransportIncomingStreamsAlgorithms> callback =
mIncomingBidirectionalAlgorithm;
LOG(("NotifyIncomingStream"));
callback->NotifyIncomingStream();
}
}
void WebTransport::NewUnidirectionalStream(
uint64_t aStreamId, const RefPtr<mozilla::ipc::DataPipeReceiver>& aStream) {
LOG_VERBOSE(("NewUnidirectionalStream()"));
// Create a Unidirectional stream and push it into the
// IncomingUnidirectionalStreams stream. Must be added to the ReceiveStreams
// array
mUnidirectionalStreams.AppendElement(
std::tuple<uint64_t, RefPtr<mozilla::ipc::DataPipeReceiver>>(aStreamId,
aStream));
// Notify something to wake up readers of IncomingReceiveStreams
// The callback is always set/used from the same thread (MainThread or a
// Worker thread).
if (mIncomingUnidirectionalAlgorithm) {
RefPtr<WebTransportIncomingStreamsAlgorithms> callback =
mIncomingUnidirectionalAlgorithm;
LOG(("NotifyIncomingStream"));
callback->NotifyIncomingStream();
}
}
void WebTransport::NewDatagramReceived(nsTArray<uint8_t>&& aData,
const mozilla::TimeStamp& aTimeStamp) {
mDatagrams->NewDatagramReceived(std::move(aData), aTimeStamp);
}
// WebIDL Boilerplate
nsIGlobalObject* WebTransport::GetParentObject() const { return mGlobal; }
JSObject* WebTransport::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return WebTransport_Binding::Wrap(aCx, this, aGivenProto);
}
// WebIDL Interface
/* static */
already_AddRefed<WebTransport> WebTransport::Constructor(
const GlobalObject& aGlobal, const nsAString& aURL,
const WebTransportOptions& aOptions, ErrorResult& aError) {
LOG(("Creating WebTransport for %s", NS_ConvertUTF16toUTF8(aURL).get()));
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<WebTransport> result = new WebTransport(global);
result->Init(aGlobal, aURL, aOptions, aError);
if (aError.Failed()) {
return nullptr;
}
// Don't let this document go into BFCache
result->NotifyToWindow(true);
// Step 25 Return transport
return result.forget();
}
void WebTransport::Init(const GlobalObject& aGlobal, const nsAString& aURL,
const WebTransportOptions& aOptions,
ErrorResult& aError) {
// Initiate connection with parent
using mozilla::ipc::BackgroundChild;
using mozilla::ipc::Endpoint;
using mozilla::ipc::PBackgroundChild;
// Steps 1-4: Parse string for validity and Throw a SyntaxError if it isn't
// Let parsedURL be the URL record resulting from parsing url.
// If parsedURL is a failure, throw a SyntaxError exception.
// If parsedURL scheme is not https, throw a SyntaxError exception.
// If parsedURL fragment is not null, throw a SyntaxError exception.
if (!ParseURL(aURL)) {
aError.ThrowSyntaxError("Invalid WebTransport URL");
return;
}
// Step 5: Let allowPooling be options's allowPooling if it exists, and false
// otherwise.
// Step 6: Let dedicated be the negation of allowPooling.
bool dedicated = !aOptions.mAllowPooling;
// Step 7: Let serverCertificateHashes be options's serverCertificateHashes if
// it exists, and null otherwise.
// Step 8: If dedicated is false and serverCertificateHashes is non-null,
// then throw a TypeError.
nsTArray<mozilla::ipc::WebTransportHash> aServerCertHashes;
if (aOptions.mServerCertificateHashes.WasPassed()) {
if (!dedicated) {
aError.ThrowNotSupportedError(
"serverCertificateHashes not supported for non-dedicated "
"connections");
return;
}
for (const auto& hash : aOptions.mServerCertificateHashes.Value()) {
if (!hash.mAlgorithm.WasPassed() || !hash.mValue.WasPassed()) continue;
if (hash.mAlgorithm.Value() != u"sha-256") {
LOG(("Algorithms other than SHA-256 are not supported"));
continue;
}
nsTArray<uint8_t> data;
if (!AppendTypedArrayDataTo(hash.mValue.Value(), data)) {
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
return;
}
nsCString alg = NS_ConvertUTF16toUTF8(hash.mAlgorithm.Value());
aServerCertHashes.EmplaceBack(alg, data);
}
}
// Step 9: Let requireUnreliable be options's requireUnreliable.
bool requireUnreliable = aOptions.mRequireUnreliable;
// Step 10: Let congestionControl be options's congestionControl.
// Step 11: If congestionControl is not "default", and the user agent
// does not support any congestion control algorithms that optimize for
// congestionControl, as allowed by [RFC9002] section 7, then set
// congestionControl to "default".
WebTransportCongestionControl congestionControl =
WebTransportCongestionControl::Default; // aOptions.mCongestionControl;
// Set this to 'default' until we add congestion control setting
// Setup up WebTransportDatagramDuplexStream
// Step 12: Let incomingDatagrams be a new ReadableStream.
// Step 13: Let outgoingDatagrams be a new WritableStream.
// Step 14: Let datagrams be the result of creating a
// WebTransportDatagramDuplexStream, its readable set to
// incomingDatagrams and its writable set to outgoingDatagrams.
mDatagrams = new WebTransportDatagramDuplexStream(mGlobal, this);
mDatagrams->Init(aError);
if (aError.Failed()) {
return;
}
// XXX TODO
// Step 15 Let transport be a newly constructed WebTransport object, with:
// SendStreams: empty ordered set
// ReceiveStreams: empty ordered set
// Ready: new promise
mReady = Promise::CreateInfallible(mGlobal);
// Closed: new promise
mClosed = Promise::CreateInfallible(mGlobal);
PBackgroundChild* backgroundChild =
BackgroundChild::GetOrCreateForCurrentThread();
if (NS_WARN_IF(!backgroundChild)) {
return;
}
nsCOMPtr<nsIPrincipal> principal = mGlobal->PrincipalOrNull();
mozilla::Maybe<IPCClientInfo> ipcClientInfo;
if (mGlobal->GetClientInfo().isSome()) {
ipcClientInfo = mozilla::Some(mGlobal->GetClientInfo().ref().ToIPC());
}
// Create a new IPC connection
Endpoint<PWebTransportParent> parentEndpoint;
Endpoint<PWebTransportChild> childEndpoint;
MOZ_ALWAYS_SUCCEEDS(
PWebTransport::CreateEndpoints(&parentEndpoint, &childEndpoint));
RefPtr<WebTransportChild> child = new WebTransportChild(this);
if (NS_IsMainThread()) {
if (!childEndpoint.Bind(child)) {
return;
}
} else if (!childEndpoint.Bind(child, mGlobal->SerialEventTarget())) {
return;
}
mState = WebTransportState::CONNECTING;
JSContext* cx = aGlobal.Context();
// Set up Datagram streams
// Step 16: Let pullDatagramsAlgorithm be an action that runs pullDatagrams
// with transport.
// Step 17: Let writeDatagramsAlgorithm be an action that runs writeDatagrams
// with transport.
// Step 18: Set up incomingDatagrams with pullAlgorithm set to
// pullDatagramsAlgorithm, and highWaterMark set to 0.
// Step 19: Set up outgoingDatagrams with writeAlgorithm set to
// writeDatagramsAlgorithm.
// XXX TODO
// Step 20: Let pullBidirectionalStreamAlgorithm be an action that runs
// pullBidirectionalStream with transport.
// Step 21: Set up transport.[[IncomingBidirectionalStreams]] with
// pullAlgorithm set to pullBidirectionalStreamAlgorithm, and highWaterMark
// set to 0.
Optional<JS::Handle<JSObject*>> underlying;
// Suppress warnings about risk of mGlobal getting nulled during script.
// We set the global from the aGlobalObject parameter of the constructor, so
// it must still be set here.
const nsCOMPtr<nsIGlobalObject> global(mGlobal);
mIncomingBidirectionalAlgorithm = new WebTransportIncomingStreamsAlgorithms(
WebTransportIncomingStreamsAlgorithms::StreamType::Bidirectional, this);
RefPtr<WebTransportIncomingStreamsAlgorithms> algorithm =
mIncomingBidirectionalAlgorithm;
mIncomingBidirectionalStreams = ReadableStream::CreateNative(
cx, global, *algorithm, Some(0.0), nullptr, aError);
if (aError.Failed()) {
return;
}
// Step 22: Let pullUnidirectionalStreamAlgorithm be an action that runs
// pullUnidirectionalStream with transport.
// Step 23: Set up transport.[[IncomingUnidirectionalStreams]] with
// pullAlgorithm set to pullUnidirectionalStreamAlgorithm, and highWaterMark
// set to 0.
mIncomingUnidirectionalAlgorithm = new WebTransportIncomingStreamsAlgorithms(
WebTransportIncomingStreamsAlgorithms::StreamType::Unidirectional, this);
algorithm = mIncomingUnidirectionalAlgorithm;
mIncomingUnidirectionalStreams = ReadableStream::CreateNative(
cx, global, *algorithm, Some(0.0), nullptr, aError);
if (aError.Failed()) {
return;
}
// Step 24: Initialize WebTransport over HTTP with transport, parsedURL,
// dedicated, requireUnreliable, and congestionControl.
LOG(("Connecting WebTransport to parent for %s",
NS_ConvertUTF16toUTF8(aURL).get()));
mChild = child;
backgroundChild
->SendCreateWebTransportParent(
aURL, principal, ipcClientInfo, dedicated, requireUnreliable,
(uint32_t)congestionControl, std::move(aServerCertHashes),
std::move(parentEndpoint))
->Then(GetCurrentSerialEventTarget(), __func__,
[self = RefPtr{this}](
PBackgroundChild::CreateWebTransportParentPromise::
ResolveOrRejectValue&& aResult) {
// aResult is a std::tuple<nsresult, uint8_t>
// TODO: is there a better/more-spec-compliant error in the
// reject case? Which begs the question, why would we get a
// reject?
nsresult rv = aResult.IsReject()
? NS_ERROR_FAILURE
: std::get<0>(aResult.ResolveValue());
LOG(("isreject: %d nsresult 0x%x", aResult.IsReject(),
(uint32_t)rv));
if (NS_FAILED(rv)) {
self->RejectWaitingConnection(rv);
} else {
// This will process anything waiting for the connection to
// complete;
self->ResolveWaitingConnection(
static_cast<WebTransportReliabilityMode>(
std::get<1>(aResult.ResolveValue())));
}
});
}
void WebTransport::ResolveWaitingConnection(
WebTransportReliabilityMode aReliability) {
LOG(("Resolved Connection %p, reliability = %u", this,
(unsigned)aReliability));
// Step 17 of initialize WebTransport over HTTP
// Step 17.1 If transport.[[State]] is not "connecting":
if (mState != WebTransportState::CONNECTING) {
// Step 17.1.1: In parallel, terminate session.
// Step 17.1.2: abort these steps
// Cleanup should have been called, which means Ready has been rejected
return;
}
// Step 17.2: Set transport.[[State]] to "connected".
mState = WebTransportState::CONNECTED;
// Step 17.3: Set transport.[[Session]] to session.
// Step 17.4: Set transport’s [[Reliability]] to "supports-unreliable".
mReliability = aReliability;
mChild->SendGetMaxDatagramSize()->Then(
GetCurrentSerialEventTarget(), __func__,
[self = RefPtr{this}](uint64_t&& aMaxDatagramSize) {
MOZ_ASSERT(self->mDatagrams);
self->mDatagrams->SetMaxDatagramSize(aMaxDatagramSize);
LOG(("max datagram size for the session is %" PRIu64,
aMaxDatagramSize));
},
[](const mozilla::ipc::ResponseRejectReason& aReason) {
LOG(("WebTransport fetching maxDatagramSize failed"));
});
// Step 17.5: Resolve transport.[[Ready]] with undefined.
mReady->MaybeResolveWithUndefined();
// We can now release any queued datagrams
mDatagrams->SetChild(mChild);
}
void WebTransport::RejectWaitingConnection(nsresult aRv) {
LOG(("Rejected connection %p %x", this, (uint32_t)aRv));
// Step 10: If connection is failure, then abort the remaining steps and
// queue a network task with transport to run these steps:
// Step 10.1: If transport.[[State]] is "closed" or "failed", then abort
// these steps.
// Step 14: If the previous step fails, abort the remaining steps and
// queue a network task with transport to run these steps:
// Step 14.1: If transport.[[State]] is "closed" or "failed", then abort
// these steps.
if (mState == WebTransportState::CLOSED ||
mState == WebTransportState::FAILED) {
if (mChild) {
mChild->Shutdown(true);
mChild = nullptr;
}
// Cleanup should have been called, which means Ready has been
// rejected and pulls resolved
return;
}
// Step 14.2: Let error be the result of creating a WebTransportError with
// "session".
RefPtr<WebTransportError> error = new WebTransportError(
"WebTransport connection rejected"_ns, WebTransportErrorSource::Session);
// Step 14.3: Cleanup transport with error.
Cleanup(error, nullptr, IgnoreErrors());
mChild->Shutdown(true);
mChild = nullptr;
}
bool WebTransport::ParseURL(const nsAString& aURL) const {
NS_ENSURE_TRUE(!aURL.IsEmpty(), false);
// 5.4 #1 and #2
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL);
NS_ENSURE_SUCCESS(rv, false);
// 5.4 #3
if (!uri->SchemeIs("https")) {
return false;
}
// 5.4 #4 no fragments
bool hasRef;
rv = uri->GetHasRef(&hasRef);
NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && !hasRef, false);
return true;
}
already_AddRefed<Promise> WebTransport::GetStats(ErrorResult& aError) {
aError.Throw(NS_ERROR_NOT_IMPLEMENTED);
return nullptr;
}
WebTransportReliabilityMode WebTransport::Reliability() { return mReliability; }
WebTransportCongestionControl WebTransport::CongestionControl() {
// XXX not implemented
return WebTransportCongestionControl::Default;
}
void WebTransport::RemoteClosed(bool aCleanly, const uint32_t& aCode,
const nsACString& aReason) {
LOG(("Server closed: cleanly: %d, code %u, reason %s", aCleanly, aCode,
PromiseFlatCString(aReason).get()));
// We calculate cleanly on the parent
// Step 2.1: If transport.[[State]] is "closed" or "failed", abort these
// steps.
if (mState == WebTransportState::CLOSED ||
mState == WebTransportState::FAILED) {
return;
}
// Step 2.2: Let error be the result of creating a WebTransportError with
// "session".
RefPtr<WebTransportError> error = new WebTransportError(
"remote WebTransport close"_ns, WebTransportErrorSource::Session);
// Step 2.3: If cleanly is false, then cleanup transport with error, and
// abort these steps.
ErrorResult errorresult;
if (!aCleanly) {
Cleanup(error, nullptr, errorresult);
return;
}
// Step 2.4: Let closeInfo be a new WebTransportCloseInfo.
// Step 2.5: If code is given, set closeInfo’s closeCode to code.
// Step 2.6: If reasonBytes is given, set closeInfo’s reason to reasonBytes,
// UTF-8 decoded.
WebTransportCloseInfo closeinfo;
closeinfo.mCloseCode = aCode;
closeinfo.mReason = aReason;
// Step 2.7: Cleanup transport with error and closeInfo.
Cleanup(error, &closeinfo, errorresult);
}
template <typename Stream>
void WebTransport::PropagateError(Stream* aStream, WebTransportError* aError) {
ErrorResult rv;
AutoJSAPI jsapi;
if (!jsapi.Init(mGlobal)) {
rv.ThrowUnknownError("Internal error");
return;
}
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> errorValue(cx);
bool ok = ToJSValue(cx, aError, &errorValue);
if (!ok) {
rv.ThrowUnknownError("Internal error");
return;
}
aStream->ErrorNative(cx, errorValue, IgnoreErrors());
}
void WebTransport::OnStreamResetOrStopSending(
uint64_t aStreamId, const StreamResetOrStopSendingError& aError) {
LOG(("WebTransport::OnStreamResetOrStopSending %p id=%" PRIx64, this,
aStreamId));
if (aError.type() == StreamResetOrStopSendingError::TStopSendingError) {
RefPtr<WebTransportSendStream> stream = mSendStreams.Get(aStreamId);
if (!stream) {
return;
}
uint8_t errorCode = net::GetWebTransportErrorFromNSResult(
aError.get_StopSendingError().error());
RefPtr<WebTransportError> error = new WebTransportError(
"WebTransportStream StopSending"_ns, WebTransportErrorSource::Stream,
Nullable<uint8_t>(errorCode));
PropagateError(stream.get(), error);
} else if (aError.type() == StreamResetOrStopSendingError::TResetError) {
RefPtr<WebTransportReceiveStream> stream = mReceiveStreams.Get(aStreamId);
LOG(("WebTransport::OnStreamResetOrStopSending reset %p stream=%p", this,
stream.get()));
if (!stream) {
return;
}
uint8_t errorCode =
net::GetWebTransportErrorFromNSResult(aError.get_ResetError().error());
RefPtr<WebTransportError> error = new WebTransportError(
"WebTransportStream Reset"_ns, WebTransportErrorSource::Stream,
Nullable<uint8_t>(errorCode));
PropagateError(stream.get(), error);
}
}
void WebTransport::Close(const WebTransportCloseInfo& aOptions,
ErrorResult& aRv) {
LOG(("Close() called"));
// Step 1 and Step 2: If transport.[[State]] is "closed" or "failed", then
// abort these steps.
if (mState == WebTransportState::CLOSED ||
mState == WebTransportState::FAILED) {
return;
}
// Step 3: If transport.[[State]] is "connecting":
if (mState == WebTransportState::CONNECTING) {
// Step 3.1: Let error be the result of creating a WebTransportError with
// "session".
RefPtr<WebTransportError> error = new WebTransportError(
"close() called on WebTransport while connecting"_ns,
WebTransportErrorSource::Session);
// Step 3.2: Cleanup transport with error.
Cleanup(error, nullptr, aRv);
// Step 3.3: Abort these steps.
mChild->Shutdown(true);
mChild = nullptr;
return;
}
LOG(("Sending Close"));
MOZ_ASSERT(mChild);
// Step 4: Let session be transport.[[Session]].
// Step 5: Let code be closeInfo.closeCode.
// Step 6: "Let reasonString be the maximal code unit prefix of
// closeInfo.reason where the length of the UTF-8 encoded prefix
// doesn’t exceed 1024."
// Take the maximal "code unit prefix" of mReason and limit to 1024 bytes
// Step 7: Let reason be reasonString, UTF-8 encoded.
// Step 8: In parallel, terminate session with code and reason.
if (aOptions.mReason.Length() > 1024u) {
// We want to start looking for the previous code point at one past the
// limit, since if a code point ends exactly at the specified length, the
// next byte will be the start of a new code point. Note
// RewindToPriorUTF8Codepoint doesn't reduce the index if it points to the
// start of a code point. We know reason[1024] is accessible since
// Length() > 1024
mChild->SendClose(
aOptions.mCloseCode,
Substring(aOptions.mReason, 0,
RewindToPriorUTF8Codepoint(aOptions.mReason.get(), 1024u)));
} else {
mChild->SendClose(aOptions.mCloseCode, aOptions.mReason);
LOG(("Close sent"));
}
// Step 9: Cleanup transport with AbortError and closeInfo. (sets mState to
// Closed)
RefPtr<WebTransportError> error =
new WebTransportError("close()"_ns, WebTransportErrorSource::Session,
DOMException_Binding::ABORT_ERR);
Cleanup(error, &aOptions, aRv);
LOG(("Cleanup done"));
// The other side will call `Close()` for us now, make sure we don't call it
// in our destructor.
mChild->Shutdown(false);
mChild = nullptr;
LOG(("Close done"));
}
already_AddRefed<WebTransportDatagramDuplexStream> WebTransport::GetDatagrams(
ErrorResult& aError) {
return do_AddRef(mDatagrams);
}
already_AddRefed<Promise> WebTransport::CreateBidirectionalStream(
const WebTransportSendStreamOptions& aOptions, ErrorResult& aRv) {
LOG(("CreateBidirectionalStream() called"));
RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject());
// Step 2: If transport.[[State]] is "closed" or "failed", return a new
// rejected promise with an InvalidStateError.
if (mState == WebTransportState::CLOSED ||
mState == WebTransportState::FAILED || !mChild) {
aRv.ThrowInvalidStateError("WebTransport closed or failed");
return nullptr;
}
// Step 3: Let sendOrder be options's sendOrder.
Maybe<int64_t> sendOrder;
if (!aOptions.mSendOrder.IsNull()) {
sendOrder = Some(aOptions.mSendOrder.Value());
}
// Step 4: Let p be a new promise.
// Step 5: Run the following steps in parallel, but abort them whenever
// transport’s [[State]] becomes "closed" or "failed", and instead queue
// a network task with transport to reject p with an InvalidStateError.
// Ask the parent to create the stream and send us the DataPipeSender/Receiver
// pair
mChild->SendCreateBidirectionalStream(
sendOrder,
[self = RefPtr{this}, sendOrder, promise](
BidirectionalStreamResponse&& aPipes) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
LOG(("CreateBidirectionalStream response"));
if (BidirectionalStreamResponse::Tnsresult == aPipes.type()) {
promise->MaybeReject(aPipes.get_nsresult());
return;
}
// Step 5.2.1: If transport.[[State]] is "closed" or "failed",
// reject p with an InvalidStateError and abort these steps.
if (BidirectionalStreamResponse::Tnsresult == aPipes.type()) {
promise->MaybeReject(aPipes.get_nsresult());
return;
}
if (self->mState == WebTransportState::CLOSED ||
self->mState == WebTransportState::FAILED) {
promise->MaybeRejectWithInvalidStateError(
"Transport close/errored before CreateBidirectional finished");
return;
}
uint64_t id = aPipes.get_BidirectionalStream().streamId();
LOG(("Create WebTransportBidirectionalStream id=%" PRIx64, id));
ErrorResult error;
RefPtr<WebTransportBidirectionalStream> newStream =
WebTransportBidirectionalStream::Create(
self, self->mGlobal, id,
aPipes.get_BidirectionalStream().inStream(),
aPipes.get_BidirectionalStream().outStream(), sendOrder, error);
LOG(("Returning a bidirectionalStream"));
promise->MaybeResolve(newStream);
},
[self = RefPtr{this}, promise](mozilla::ipc::ResponseRejectReason) {
LOG(("CreateBidirectionalStream reject"));
promise->MaybeRejectWithInvalidStateError(
"Transport close/errored before CreateBidirectional started");
});
// Step 6: return p
return promise.forget();
}
already_AddRefed<ReadableStream> WebTransport::IncomingBidirectionalStreams() {
return do_AddRef(mIncomingBidirectionalStreams);
}
already_AddRefed<Promise> WebTransport::CreateUnidirectionalStream(
const WebTransportSendStreamOptions& aOptions, ErrorResult& aRv) {
LOG(("CreateUnidirectionalStream() called"));
// Step 2: If transport.[[State]] is "closed" or "failed", return a new
// rejected promise with an InvalidStateError.
if (mState == WebTransportState::CLOSED ||
mState == WebTransportState::FAILED || !mChild) {
aRv.ThrowInvalidStateError("WebTransport closed or failed");
return nullptr;
}
// Step 3: Let sendOrder be options's sendOrder.
Maybe<int64_t> sendOrder;
if (!aOptions.mSendOrder.IsNull()) {
sendOrder = Some(aOptions.mSendOrder.Value());
}
// Step 4: Let p be a new promise.
RefPtr<Promise> promise = Promise::CreateInfallible(GetParentObject());
// Step 5: Run the following steps in parallel, but abort them whenever
// transport’s [[State]] becomes "closed" or "failed", and instead queue
// a network task with transport to reject p with an InvalidStateError.
// Ask the parent to create the stream and send us the DataPipeSender
mChild->SendCreateUnidirectionalStream(
sendOrder,
[self = RefPtr{this}, sendOrder,
promise](UnidirectionalStreamResponse&& aResponse)
MOZ_CAN_RUN_SCRIPT_BOUNDARY {
LOG(("CreateUnidirectionalStream response"));
if (UnidirectionalStreamResponse::Tnsresult == aResponse.type()) {
promise->MaybeReject(aResponse.get_nsresult());
return;
}
// Step 5.1: Let internalStream be the result of creating an
// outgoing unidirectional stream with transport.[[Session]].
// Step 5.2: Queue a network task with transport to run the
// following steps:
// Step 5.2.1 If transport.[[State]] is "closed" or "failed",
// reject p with an InvalidStateError and abort these steps.
if (self->mState == WebTransportState::CLOSED ||
self->mState == WebTransportState::FAILED ||
aResponse.type() !=
UnidirectionalStreamResponse::TUnidirectionalStream) {
promise->MaybeRejectWithInvalidStateError(
"Transport close/errored during CreateUnidirectional");
return;
}
// Step 5.2.2.: Let stream be the result of creating a
// WebTransportSendStream with internalStream, transport, and
// sendOrder.
ErrorResult error;
uint64_t id = aResponse.get_UnidirectionalStream().streamId();
LOG(("Create WebTransportSendStream id=%" PRIx64, id));
RefPtr<WebTransportSendStream> writableStream =
WebTransportSendStream::Create(
self, self->mGlobal, id,
aResponse.get_UnidirectionalStream().outStream(), sendOrder,
error);
if (!writableStream) {
promise->MaybeReject(std::move(error));
return;
}
LOG(("Returning a writableStream"));
// Step 5.2.3: Resolve p with stream.
promise->MaybeResolve(writableStream);
},
[self = RefPtr{this}, promise](mozilla::ipc::ResponseRejectReason) {
LOG(("CreateUnidirectionalStream reject"));
promise->MaybeRejectWithInvalidStateError(
"Transport close/errored during CreateUnidirectional");
});
// Step 6: return p
return promise.forget();
}
already_AddRefed<ReadableStream> WebTransport::IncomingUnidirectionalStreams() {
return do_AddRef(mIncomingUnidirectionalStreams);
}
// Can be invoked with "error", "error, error, and true/false", or "error and
// closeInfo", but reason and abruptly are never used, and it does use closeinfo
void WebTransport::Cleanup(WebTransportError* aError,
const WebTransportCloseInfo* aCloseInfo,
ErrorResult& aRv) {
// Step 1: Let sendStreams be a copy of transport.[[SendStreams]]
// Step 2: Let receiveStreams be a copy of transport.[[ReceiveStreams]]
// Step 3: Let ready be transport.[[Ready]] -> (mReady)
// Step 4: Let closed be transport.[[Closed]] -> (mClosed)
// Step 5: Let incomingBidirectionalStreams be
// transport.[[IncomingBidirectionalStreams]].
// Step 6: Let incomingUnidirectionalStreams be
// transport.[[IncomingUnidirectionalStreams]].
// Step 7: Set transport.[[SendStreams]] to an empty set.
// Step 8: Set transport.[[ReceiveStreams]] to an empty set.
LOG(("Cleanup started"));
nsTHashMap<uint64_t, RefPtr<WebTransportSendStream>> sendStreams;
sendStreams.SwapElements(mSendStreams);
nsTHashMap<uint64_t, RefPtr<WebTransportReceiveStream>> receiveStreams;
receiveStreams.SwapElements(mReceiveStreams);
// Step 9: If closeInfo is given, then set transport.[[State]] to "closed".
// Otherwise, set transport.[[State]] to "failed".
mState = aCloseInfo ? WebTransportState::CLOSED : WebTransportState::FAILED;
// Step 10: For each sendStream in sendStreams, error sendStream with error.
AutoJSAPI jsapi;
if (!jsapi.Init(mGlobal)) {
aRv.ThrowUnknownError("Internal error");
return;
}
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> errorValue(cx);
bool ok = ToJSValue(cx, aError, &errorValue);
if (!ok) {
aRv.ThrowUnknownError("Internal error");
return;
}
for (const auto& stream : sendStreams.Values()) {
MOZ_KnownLive(stream)->ErrorNative(cx, errorValue, IgnoreErrors());
}
// Step 11: For each receiveStream in receiveStreams, error receiveStream with
// error.
for (const auto& stream : receiveStreams.Values()) {
stream->ErrorNative(cx, errorValue, IgnoreErrors());
}
// Step 12:
if (aCloseInfo) {
// 12.1: Resolve closed with closeInfo.
LOG(("Resolving mClosed with closeinfo"));
mClosed->MaybeResolve(*aCloseInfo);
// 12.2: Assert: ready is settled.
MOZ_ASSERT(mReady->State() != Promise::PromiseState::Pending);
// 12.3: Close incomingBidirectionalStreams
// This keeps the clang-plugin happy
RefPtr<ReadableStream> stream = mIncomingBidirectionalStreams;
stream->CloseNative(cx, IgnoreErrors());
// 12.4: Close incomingUnidirectionalStreams
stream = mIncomingUnidirectionalStreams;
stream->CloseNative(cx, IgnoreErrors());
} else {
// Step 13
// 13.1: Reject closed with error
LOG(("Rejecting mClosed"));
mClosed->MaybeReject(errorValue);
// 13.2: Reject ready with error
mReady->MaybeReject(errorValue);
// 13.3: Error incomingBidirectionalStreams with error
mIncomingBidirectionalStreams->ErrorNative(cx, errorValue, IgnoreErrors());
// 13.4: Error incomingUnidirectionalStreams with error
mIncomingUnidirectionalStreams->ErrorNative(cx, errorValue, IgnoreErrors());
}
// Let go of the algorithms
mIncomingBidirectionalAlgorithm = nullptr;
mIncomingUnidirectionalAlgorithm = nullptr;
// We no longer block BFCache
NotifyToWindow(false);
}
void WebTransport::SendSetSendOrder(uint64_t aStreamId,
Maybe<int64_t> aSendOrder) {
mChild->SendSetSendOrder(aStreamId, aSendOrder);
}
void WebTransport::NotifyBFCacheOnMainThread(nsPIDOMWindowInner* aInner,
bool aCreated) {
AssertIsOnMainThread();
if (!aInner) {
return;
}
if (aCreated) {
aInner->RemoveFromBFCacheSync();
}
uint32_t count = aInner->UpdateWebTransportCount(aCreated);
// It's okay for WindowGlobalChild to not exist, as it should mean it already
// is destroyed and can't enter bfcache anyway.
if (WindowGlobalChild* child = aInner->GetWindowGlobalChild()) {
if (aCreated && count == 1) {
// The first WebTransport is active.
child->BlockBFCacheFor(BFCacheStatus::ACTIVE_WEBTRANSPORT);
} else if (count == 0) {
child->UnblockBFCacheFor(BFCacheStatus::ACTIVE_WEBTRANSPORT);
}
}
}
class BFCacheNotifyWTRunnable final : public WorkerProxyToMainThreadRunnable {
public:
explicit BFCacheNotifyWTRunnable(bool aCreated) : mCreated(aCreated) {}
void RunOnMainThread(WorkerPrivate* aWorkerPrivate) override {
MOZ_ASSERT(aWorkerPrivate);
AssertIsOnMainThread();
if (aWorkerPrivate->IsDedicatedWorker()) {
WebTransport::NotifyBFCacheOnMainThread(
aWorkerPrivate->GetAncestorWindow(), mCreated);
return;
}
if (aWorkerPrivate->IsSharedWorker()) {
aWorkerPrivate->GetRemoteWorkerController()->NotifyWebTransport(mCreated);
return;
}
MOZ_ASSERT_UNREACHABLE("Unexpected worker type");
}
void RunBackOnWorkerThreadForCleanup(WorkerPrivate* aWorkerPrivate) override {
MOZ_ASSERT(aWorkerPrivate);
aWorkerPrivate->AssertIsOnWorkerThread();
}
private:
bool mCreated;
};
void WebTransport::NotifyToWindow(bool aCreated) const {
if (NS_IsMainThread()) {
NotifyBFCacheOnMainThread(GetParentObject()->GetAsInnerWindow(), aCreated);
return;
}
WorkerPrivate* wp = GetCurrentThreadWorkerPrivate();
if (wp->IsDedicatedWorker() || wp->IsSharedWorker()) {
RefPtr<BFCacheNotifyWTRunnable> runnable =
new BFCacheNotifyWTRunnable(aCreated);
runnable->Dispatch(wp);
}
};
} // namespace mozilla::dom