diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/network/UDPSocket.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/network/UDPSocket.cpp')
-rw-r--r-- | dom/network/UDPSocket.cpp | 719 |
1 files changed, 719 insertions, 0 deletions
diff --git a/dom/network/UDPSocket.cpp b/dom/network/UDPSocket.cpp new file mode 100644 index 0000000000..63f7eadb45 --- /dev/null +++ b/dom/network/UDPSocket.cpp @@ -0,0 +1,719 @@ +/* -*- 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 "UDPSocket.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/network/UDPSocketChild.h" +#include "mozilla/dom/UDPMessageEvent.h" +#include "mozilla/dom/UDPSocketBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/net/DNS.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsINetAddr.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(UDPSocket::ListenerProxy, nsIUDPSocketListener, + nsIUDPSocketInternal) + +NS_IMPL_CYCLE_COLLECTION_CLASS(UDPSocket) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(UDPSocket, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOpened) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClosed) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(UDPSocket, DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOpened) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mClosed) + tmp->CloseWithReason(NS_OK); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(UDPSocket, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(UDPSocket, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UDPSocket) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketListener) + NS_INTERFACE_MAP_ENTRY(nsIUDPSocketInternal) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +/* static */ +already_AddRefed<UDPSocket> UDPSocket::Constructor(const GlobalObject& aGlobal, + const UDPOptions& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + bool addressReuse = aOptions.mAddressReuse; + bool loopback = aOptions.mLoopback; + + nsCString remoteAddress; + if (aOptions.mRemoteAddress.WasPassed()) { + CopyUTF16toUTF8(aOptions.mRemoteAddress.Value(), remoteAddress); + } else { + remoteAddress.SetIsVoid(true); + } + + Nullable<uint16_t> remotePort; + if (aOptions.mRemotePort.WasPassed()) { + remotePort.SetValue(aOptions.mRemotePort.Value()); + + if (remotePort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + nsString localAddress; + if (aOptions.mLocalAddress.WasPassed()) { + localAddress = aOptions.mLocalAddress.Value(); + + // check if localAddress is a valid IPv4/6 address + NS_ConvertUTF16toUTF8 address(localAddress); + PRNetAddr prAddr; + PRStatus status = PR_StringToNetAddr(address.BeginReading(), &prAddr); + if (status != PR_SUCCESS) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } else { + SetDOMStringToNull(localAddress); + } + + Nullable<uint16_t> localPort; + if (aOptions.mLocalPort.WasPassed()) { + localPort.SetValue(aOptions.mLocalPort.Value()); + + if (localPort.Value() == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return nullptr; + } + } + + RefPtr<UDPSocket> socket = + new UDPSocket(ownerWindow, remoteAddress, remotePort); + aRv = socket->Init(localAddress, localPort, addressReuse, loopback); + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return socket.forget(); +} + +UDPSocket::UDPSocket(nsPIDOMWindowInner* aOwner, + const nsCString& aRemoteAddress, + const Nullable<uint16_t>& aRemotePort) + : DOMEventTargetHelper(aOwner), + mRemoteAddress(aRemoteAddress), + mRemotePort(aRemotePort), + mAddressReuse(false), + mLoopback(false), + mReadyState(SocketReadyState::Opening) { + MOZ_ASSERT(aOwner); + + Document* aDoc = aOwner->GetExtantDoc(); + if (aDoc) { + aDoc->DisallowBFCaching(); + } +} + +UDPSocket::~UDPSocket() { CloseWithReason(NS_OK); } + +JSObject* UDPSocket::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return UDPSocket_Binding::Wrap(aCx, this, aGivenProto); +} + +void UDPSocket::DisconnectFromOwner() { + DOMEventTargetHelper::DisconnectFromOwner(); + CloseWithReason(NS_OK); +} + +already_AddRefed<Promise> UDPSocket::Close() { + MOZ_ASSERT(mClosed); + + RefPtr<Promise> promise = mClosed; + + if (mReadyState == SocketReadyState::Closed) { + return promise.forget(); + } + + CloseWithReason(NS_OK); + return promise.forget(); +} + +void UDPSocket::CloseWithReason(nsresult aReason) { + if (mReadyState == SocketReadyState::Closed) { + return; + } + + if (mOpened) { + if (mReadyState == SocketReadyState::Opening) { + // reject openedPromise with AbortError if socket is closed without error + nsresult openFailedReason = + NS_FAILED(aReason) ? aReason : NS_ERROR_DOM_ABORT_ERR; + mOpened->MaybeReject(openFailedReason); + } + } + + mReadyState = SocketReadyState::Closed; + + if (mListenerProxy) { + mListenerProxy->Disconnect(); + mListenerProxy = nullptr; + } + + if (mSocket) { + mSocket->Close(); + mSocket = nullptr; + } + + if (mSocketChild) { + mSocketChild->Close(); + mSocketChild = nullptr; + } + + if (mClosed) { + if (NS_SUCCEEDED(aReason)) { + mClosed->MaybeResolveWithUndefined(); + } else { + mClosed->MaybeReject(aReason); + } + } + + mPendingMcastCommands.Clear(); +} + +void UDPSocket::JoinMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) { + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand joinCommand(MulticastCommand::Join, + aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(joinCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + NS_ConvertUTF16toUTF8 address(aMulticastGroupAddress); + + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->JoinMulticast(address, ""_ns); + NS_WARNING_ASSERTION(!aRv.Failed(), "JoinMulticast failed"); + + return; + } + + MOZ_ASSERT(mSocketChild); + + mSocketChild->JoinMulticast(address, ""_ns); +} + +void UDPSocket::LeaveMulticastGroup(const nsAString& aMulticastGroupAddress, + ErrorResult& aRv) { + if (mReadyState == SocketReadyState::Closed) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mReadyState == SocketReadyState::Opening) { + MulticastCommand leaveCommand(MulticastCommand::Leave, + aMulticastGroupAddress); + mPendingMcastCommands.AppendElement(leaveCommand); + return; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + nsCString address = NS_ConvertUTF16toUTF8(aMulticastGroupAddress); + if (mSocket) { + MOZ_ASSERT(!mSocketChild); + + aRv = mSocket->LeaveMulticast(address, ""_ns); + NS_WARNING_ASSERTION(!aRv.Failed(), "mSocket->LeaveMulticast failed"); + return; + } + + MOZ_ASSERT(mSocketChild); + + mSocketChild->LeaveMulticast(address, ""_ns); +} + +nsresult UDPSocket::DoPendingMcastCommand() { + MOZ_ASSERT(mReadyState == SocketReadyState::Open, + "Multicast command can only be executed after socket opened"); + + for (uint32_t i = 0; i < mPendingMcastCommands.Length(); ++i) { + MulticastCommand& command = mPendingMcastCommands[i]; + ErrorResult rv; + + switch (command.mCommand) { + case MulticastCommand::Join: { + JoinMulticastGroup(command.mAddress, rv); + break; + } + case MulticastCommand::Leave: { + LeaveMulticastGroup(command.mAddress, rv); + break; + } + } + + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + } + + mPendingMcastCommands.Clear(); + return NS_OK; +} + +bool UDPSocket::Send(const StringOrBlobOrArrayBufferOrArrayBufferView& aData, + const Optional<nsAString>& aRemoteAddress, + const Optional<Nullable<uint16_t>>& aRemotePort, + ErrorResult& aRv) { + if (mReadyState != SocketReadyState::Open) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + MOZ_ASSERT(mSocket || mSocketChild); + + // If the remote address and port were not specified in the constructor or as + // arguments, throw InvalidAccessError. + nsCString remoteAddress; + if (aRemoteAddress.WasPassed()) { + CopyUTF16toUTF8(aRemoteAddress.Value(), remoteAddress); + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else if (!mRemoteAddress.IsVoid()) { + remoteAddress = mRemoteAddress; + UDPSOCKET_LOG(("%s: Send to %s", __FUNCTION__, remoteAddress.get())); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + uint16_t remotePort; + if (aRemotePort.WasPassed() && !aRemotePort.Value().IsNull()) { + remotePort = aRemotePort.Value().Value(); + } else if (!mRemotePort.IsNull()) { + remotePort = mRemotePort.Value(); + } else { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return false; + } + + nsCOMPtr<nsIInputStream> stream; + if (aData.IsBlob()) { + Blob& blob = aData.GetAsBlob(); + + blob.CreateInputStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + } else { + nsresult rv; + nsCOMPtr<nsIStringInputStream> strStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return false; + } + + if (aData.IsString()) { + NS_ConvertUTF16toUTF8 data(aData.GetAsString()); + aRv = strStream->SetData(data.BeginReading(), data.Length()); + } else if (aData.IsArrayBuffer()) { + const ArrayBuffer& data = aData.GetAsArrayBuffer(); + data.ComputeState(); + aRv = strStream->SetData(reinterpret_cast<const char*>(data.Data()), + data.Length()); + } else { + const ArrayBufferView& data = aData.GetAsArrayBufferView(); + data.ComputeState(); + aRv = strStream->SetData(reinterpret_cast<const char*>(data.Data()), + data.Length()); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + stream = strStream; + } + + if (mSocket) { + aRv = mSocket->SendBinaryStream(remoteAddress, remotePort, stream); + } else if (mSocketChild) { + aRv = mSocketChild->SendBinaryStream(remoteAddress, remotePort, stream); + } + + if (NS_WARN_IF(aRv.Failed())) { + return false; + } + + return true; +} + +nsresult UDPSocket::InitLocal(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) { + nsresult rv; + + nsCOMPtr<nsIUDPSocket> sock = + do_CreateInstance("@mozilla.org/network/udp-socket;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = global->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + if (aLocalAddress.IsEmpty()) { + rv = sock->Init(aLocalPort, /* loopback = */ false, principal, + mAddressReuse, /* optionalArgc = */ 1); + } else { + PRNetAddr prAddr; + PR_InitializeNetAddr(PR_IpAddrAny, aLocalPort, &prAddr); + PR_StringToNetAddr(NS_ConvertUTF16toUTF8(aLocalAddress).BeginReading(), + &prAddr); + UDPSOCKET_LOG(("%s: %s:%u", __FUNCTION__, + NS_ConvertUTF16toUTF8(aLocalAddress).get(), aLocalPort)); + + mozilla::net::NetAddr addr(&prAddr); + rv = sock->InitWithAddress(&addr, principal, mAddressReuse, + /* optionalArgc = */ 1); + } + if (NS_FAILED(rv)) { + return rv; + } + + rv = sock->SetMulticastLoopback(mLoopback); + if (NS_FAILED(rv)) { + return rv; + } + + mSocket = sock; + + // Get real local address and local port + nsCOMPtr<nsINetAddr> localAddr; + rv = mSocket->GetLocalAddr(getter_AddRefs(localAddr)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString localAddress; + rv = localAddr->GetAddress(localAddress); + if (NS_FAILED(rv)) { + return rv; + } + CopyUTF8toUTF16(localAddress, mLocalAddress); + + uint16_t localPort; + rv = localAddr->GetPort(&localPort); + if (NS_FAILED(rv)) { + return rv; + } + mLocalPort.SetValue(localPort); + + mListenerProxy = new ListenerProxy(this); + + rv = mSocket->AsyncListen(mListenerProxy); + if (NS_FAILED(rv)) { + return rv; + } + + mReadyState = SocketReadyState::Open; + rv = DoPendingMcastCommand(); + if (NS_FAILED(rv)) { + return rv; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +nsresult UDPSocket::InitRemote(const nsAString& aLocalAddress, + const uint16_t& aLocalPort) { + nsresult rv; + + RefPtr<UDPSocketChild> sock = new UDPSocketChild(); + + mListenerProxy = new ListenerProxy(this); + + nsCOMPtr<nsIGlobalObject> obj = do_QueryInterface(GetOwner(), &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIPrincipal> principal = obj->PrincipalOrNull(); + if (!principal) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISerialEventTarget> target; + if (nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal()) { + target = global->EventTargetFor(TaskCategory::Other); + } + + rv = sock->Bind(mListenerProxy, principal, + NS_ConvertUTF16toUTF8(aLocalAddress), aLocalPort, + mAddressReuse, mLoopback, 0, 0, target); + + if (NS_FAILED(rv)) { + return rv; + } + + mSocketChild = sock; + + return NS_OK; +} + +nsresult UDPSocket::Init(const nsString& aLocalAddress, + const Nullable<uint16_t>& aLocalPort, + const bool& aAddressReuse, const bool& aLoopback) { + MOZ_ASSERT(!mSocket && !mSocketChild); + + mLocalAddress = aLocalAddress; + mLocalPort = aLocalPort; + mAddressReuse = aAddressReuse; + mLoopback = aLoopback; + + ErrorResult rv; + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetOwner()); + + mOpened = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + mClosed = Promise::Create(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + class OpenSocketRunnable final : public Runnable { + public: + explicit OpenSocketRunnable(UDPSocket* aSocket) + : mozilla::Runnable("OpenSocketRunnable"), mSocket(aSocket) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mSocket); + + if (mSocket->mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + uint16_t localPort = 0; + if (!mSocket->mLocalPort.IsNull()) { + localPort = mSocket->mLocalPort.Value(); + } + + nsresult rv; + if (!XRE_IsParentProcess()) { + rv = mSocket->InitRemote(mSocket->mLocalAddress, localPort); + } else { + rv = mSocket->InitLocal(mSocket->mLocalAddress, localPort); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + mSocket->CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + } + + return NS_OK; + } + + private: + RefPtr<UDPSocket> mSocket; + }; + + nsCOMPtr<nsIRunnable> runnable = new OpenSocketRunnable(this); + + return NS_DispatchToMainThread(runnable); +} + +void UDPSocket::HandleReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData) { + if (mReadyState != SocketReadyState::Open) { + return; + } + + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + return; + } + + if (NS_FAILED(DispatchReceivedData(aRemoteAddress, aRemotePort, aData))) { + CloseWithReason(NS_ERROR_UNEXPECTED); + } +} + +nsresult UDPSocket::DispatchReceivedData(const nsACString& aRemoteAddress, + const uint16_t& aRemotePort, + const nsTArray<uint8_t>& aData) { + AutoJSAPI jsapi; + + if (NS_WARN_IF(!jsapi.Init(GetOwner()))) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + + // Copy packet data to ArrayBuffer + JS::Rooted<JSObject*> arrayBuf( + cx, ArrayBuffer::Create(cx, aData.Length(), aData.Elements())); + + if (NS_WARN_IF(!arrayBuf)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> jsData(cx, JS::ObjectValue(*arrayBuf)); + + // Create DOM event + RootedDictionary<UDPMessageEventInit> init(cx); + CopyUTF8toUTF16(aRemoteAddress, init.mRemoteAddress); + init.mRemotePort = aRemotePort; + init.mData = jsData; + + RefPtr<UDPMessageEvent> udpEvent = + UDPMessageEvent::Constructor(this, u"message"_ns, init); + + if (NS_WARN_IF(!udpEvent)) { + return NS_ERROR_FAILURE; + } + + udpEvent->SetTrusted(true); + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, udpEvent); + + return asyncDispatcher->PostDOMEvent(); +} + +// nsIUDPSocketListener + +NS_IMETHODIMP +UDPSocket::OnPacketReceived(nsIUDPSocket* aSocket, nsIUDPMessage* aMessage) { + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + // Create appropriate JS object for message + FallibleTArray<uint8_t>& buffer = aMessage->GetDataAsTArray(); + + nsCOMPtr<nsINetAddr> addr; + if (NS_WARN_IF(NS_FAILED(aMessage->GetFromAddr(getter_AddRefs(addr))))) { + return NS_OK; + } + + nsCString remoteAddress; + if (NS_WARN_IF(NS_FAILED(addr->GetAddress(remoteAddress)))) { + return NS_OK; + } + + uint16_t remotePort; + if (NS_WARN_IF(NS_FAILED(addr->GetPort(&remotePort)))) { + return NS_OK; + } + + HandleReceivedData(remoteAddress, remotePort, buffer); + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::OnStopListening(nsIUDPSocket* aSocket, nsresult aStatus) { + // nsIUDPSocketListener callbacks should be invoked on main thread. + MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread"); + + CloseWithReason(aStatus); + + return NS_OK; +} + +// nsIUDPSocketInternal + +NS_IMETHODIMP +UDPSocket::CallListenerError(const nsACString& aMessage, + const nsACString& aFilename, + uint32_t aLineNumber) { + CloseWithReason(NS_ERROR_DOM_NETWORK_ERR); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerReceivedData(const nsACString& aRemoteAddress, + uint16_t aRemotePort, + const nsTArray<uint8_t>& aData) { + HandleReceivedData(aRemoteAddress, aRemotePort, aData); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerOpened() { + if (mReadyState != SocketReadyState::Opening) { + return NS_OK; + } + + MOZ_ASSERT(mSocketChild); + + // Get real local address and local port + CopyUTF8toUTF16(mSocketChild->LocalAddress(), mLocalAddress); + + mLocalPort.SetValue(mSocketChild->LocalPort()); + + mReadyState = SocketReadyState::Open; + nsresult rv = DoPendingMcastCommand(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseWithReason(rv); + return NS_OK; + } + + mOpened->MaybeResolveWithUndefined(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerConnected() { + // This shouldn't be called here. + MOZ_CRASH(); + + return NS_OK; +} + +NS_IMETHODIMP +UDPSocket::CallListenerClosed() { + CloseWithReason(NS_OK); + + return NS_OK; +} + +} // namespace mozilla::dom |