diff options
Diffstat (limited to 'dom/webtransport/api/WebTransport.cpp')
-rw-r--r-- | dom/webtransport/api/WebTransport.cpp | 942 |
1 files changed, 942 insertions, 0 deletions
diff --git a/dom/webtransport/api/WebTransport.cpp b/dom/webtransport/api/WebTransport.cpp new file mode 100644 index 0000000000..acd1d5a5e3 --- /dev/null +++ b/dom/webtransport/api/WebTransport.cpp @@ -0,0 +1,942 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#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: + // https://w3c.github.io/webtransport/#webtransport-procedures + 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())); + // https://w3c.github.io/webtransport/#webtransport-constructor + + 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) { + // https://w3c.github.io/webtransport/#webtransport-constructor + // Initiate connection with parent + using mozilla::ipc::BackgroundChild; + using mozilla::ipc::Endpoint; + using mozilla::ipc::PBackgroundChild; + + // https://w3c.github.io/webtransport/#webtransport-constructor + // 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. + if (aOptions.mServerCertificateHashes.WasPassed()) { + // XXX bug 1806693 + aError.ThrowNotSupportedError("No support for serverCertificateHashes yet"); + // XXX if dedicated is false and serverCertificateHashes is non-null, then + // throw a TypeError. Also should enforce in parent + return; + } + // 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->EventTargetFor(TaskCategory::Other))) { + 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())); + + // https://w3c.github.io/webtransport/#webtransport-constructor Spec 5.2 + mChild = child; + backgroundChild + ->SendCreateWebTransportParent(aURL, principal, ipcClientInfo, dedicated, + requireUnreliable, + (uint32_t)congestionControl, + // XXX serverCertHashes, + 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)); + // https://w3c.github.io/webtransport/#webtransport-constructor + // 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)); + // https://w3c.github.io/webtransport/#initialize-webtransport-over-http + + // 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) { + 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 = https://w3c.github.io/webtransport/#webtransport-constructor + // 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())); + // Step 2 of https://w3c.github.io/webtransport/#web-transport-termination + // 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")); + // https://w3c.github.io/webtransport/#dom-webtransport-close + // 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")); + // https://w3c.github.io/webtransport/#dom-webtransport-createbidirectionalstream + 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}, 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(), 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")); + // https://w3c.github.io/webtransport/#dom-webtransport-createunidirectionalstream + // 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}, 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(), 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) { + // https://w3c.github.io/webtransport/#webtransport-cleanup + // 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()) { + // This MOZ_KnownLive is redundant, see bug 1620312 + 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::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()->AsInnerWindow(), aCreated); + return; + } + + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (wp->IsDedicatedWorker() || wp->IsSharedWorker()) { + RefPtr<BFCacheNotifyWTRunnable> runnable = + new BFCacheNotifyWTRunnable(aCreated); + + runnable->Dispatch(wp); + } +}; + +} // namespace mozilla::dom |