diff options
Diffstat (limited to 'dom/file/uri/BlobURLProtocolHandler.cpp')
-rw-r--r-- | dom/file/uri/BlobURLProtocolHandler.cpp | 990 |
1 files changed, 990 insertions, 0 deletions
diff --git a/dom/file/uri/BlobURLProtocolHandler.cpp b/dom/file/uri/BlobURLProtocolHandler.cpp new file mode 100644 index 0000000000..756ce01544 --- /dev/null +++ b/dom/file/uri/BlobURLProtocolHandler.cpp @@ -0,0 +1,990 @@ +/* -*- 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 "BlobURLProtocolHandler.h" +#include "BlobURLChannel.h" +#include "mozilla/dom/BlobURL.h" + +#include "mozilla/dom/ChromeUtils.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/MediaSource.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/Maybe.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/Preferences.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "nsClassHashtable.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsIAsyncShutdown.h" +#include "nsIDUtils.h" +#include "nsIException.h" // for nsIStackFrame +#include "nsIMemoryReporter.h" +#include "nsIPrincipal.h" +#include "nsIUUIDGenerator.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" + +#define RELEASING_TIMER 5000 + +namespace mozilla { + +using namespace ipc; + +namespace dom { + +// ----------------------------------------------------------------------- +// Hash table +struct DataInfo { + enum ObjectType { eBlobImpl, eMediaSource }; + + DataInfo(mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) + : mObjectType(eBlobImpl), + mBlobImpl(aBlobImpl), + mPrincipal(aPrincipal), + mAgentClusterId(aAgentClusterId), + mRevoked(false) { + MOZ_ASSERT(aPrincipal); + } + + DataInfo(MediaSource* aMediaSource, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) + : mObjectType(eMediaSource), + mMediaSource(aMediaSource), + mPrincipal(aPrincipal), + mAgentClusterId(aAgentClusterId), + mRevoked(false) { + MOZ_ASSERT(aPrincipal); + } + + ObjectType mObjectType; + + RefPtr<BlobImpl> mBlobImpl; + RefPtr<MediaSource> mMediaSource; + + nsCOMPtr<nsIPrincipal> mPrincipal; + Maybe<nsID> mAgentClusterId; + + nsCString mStack; + + // When a blobURL is revoked, we keep it alive for RELEASING_TIMER + // milliseconds in order to support pending operations such as navigation, + // download and so on. + bool mRevoked; +}; + +// The mutex is locked whenever gDataTable is changed, or if gDataTable +// is accessed off-main-thread. +static StaticMutex sMutex MOZ_UNANNOTATED; + +// All changes to gDataTable must happen on the main thread, while locking +// sMutex. Reading from gDataTable on the main thread may happen without +// locking, since no changes are possible. Reading it from another thread +// must also lock sMutex to prevent data races. +static nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>* gDataTable; + +static mozilla::dom::DataInfo* GetDataInfo(const nsACString& aUri, + bool aAlsoIfRevoked = false) { + if (!gDataTable) { + return nullptr; + } + + // Let's remove any fragment from this URI. + int32_t fragmentPos = aUri.FindChar('#'); + + mozilla::dom::DataInfo* res; + if (fragmentPos < 0) { + res = gDataTable->Get(aUri); + } else { + res = gDataTable->Get(StringHead(aUri, fragmentPos)); + } + + if (!aAlsoIfRevoked && res && res->mRevoked) { + return nullptr; + } + + return res; +} + +static mozilla::dom::DataInfo* GetDataInfoFromURI(nsIURI* aURI, + bool aAlsoIfRevoked = false) { + if (!aURI) { + return nullptr; + } + + nsCString spec; + nsresult rv = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetDataInfo(spec, aAlsoIfRevoked); +} + +// Memory reporting for the hash table. +void BroadcastBlobURLRegistration(const nsACString& aURI, + mozilla::dom::BlobImpl* aBlobImpl, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aPrincipal); + + if (XRE_IsParentProcess()) { + dom::ContentParent::BroadcastBlobURLRegistration( + aURI, aBlobImpl, aPrincipal, aAgentClusterId); + return; + } + + IPCBlob ipcBlob; + nsresult rv = IPCBlobUtils::Serialize(aBlobImpl, ipcBlob); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + dom::ContentChild* cc = dom::ContentChild::GetSingleton(); + (void)NS_WARN_IF(!cc->SendStoreAndBroadcastBlobURLRegistration( + nsCString(aURI), ipcBlob, aPrincipal, aAgentClusterId)); +} + +void BroadcastBlobURLUnregistration(const nsCString& aURI, + nsIPrincipal* aPrincipal) { + MOZ_ASSERT(NS_IsMainThread()); + + if (XRE_IsParentProcess()) { + dom::ContentParent::BroadcastBlobURLUnregistration(aURI, aPrincipal); + return; + } + + dom::ContentChild* cc = dom::ContentChild::GetSingleton(); + if (cc) { + (void)NS_WARN_IF( + !cc->SendUnstoreAndBroadcastBlobURLUnregistration(aURI, aPrincipal)); + } +} + +class BlobURLsReporter final : public nsIMemoryReporter { + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aCallback, + nsISupports* aData, bool aAnonymize) override { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + if (!gDataTable) { + return NS_OK; + } + + nsTHashMap<nsPtrHashKey<mozilla::dom::BlobImpl>, uint32_t> refCounts; + + // Determine number of URLs per mozilla::dom::BlobImpl, to handle the case + // where it's > 1. + for (const auto& entry : *gDataTable) { + if (entry.GetWeak()->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + continue; + } + + mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl; + MOZ_ASSERT(blobImpl); + + refCounts.LookupOrInsert(blobImpl, 0) += 1; + } + + for (const auto& entry : *gDataTable) { + nsCStringHashKey::KeyType key = entry.GetKey(); + mozilla::dom::DataInfo* info = entry.GetWeak(); + + if (entry.GetWeak()->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + mozilla::dom::BlobImpl* blobImpl = entry.GetWeak()->mBlobImpl; + MOZ_ASSERT(blobImpl); + + constexpr auto desc = + "A blob URL allocated with URL.createObjectURL; the referenced " + "blob cannot be freed until all URLs for it have been explicitly " + "invalidated with URL.revokeObjectURL."_ns; + nsAutoCString path, url, owner, specialDesc; + uint64_t size = 0; + uint32_t refCount = 1; + DebugOnly<bool> blobImplWasCounted; + + blobImplWasCounted = refCounts.Get(blobImpl, &refCount); + MOZ_ASSERT(blobImplWasCounted); + MOZ_ASSERT(refCount > 0); + + bool isMemoryFile = blobImpl->IsMemoryFile(); + + if (isMemoryFile) { + ErrorResult rv; + size = blobImpl->GetSize(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + size = 0; + } + } + + path = isMemoryFile ? "memory-blob-urls/" : "file-blob-urls/"; + BuildPath(path, key, info, aAnonymize); + + if (refCount > 1) { + nsAutoCString addrStr; + + addrStr = "0x"; + addrStr.AppendInt((uint64_t)(mozilla::dom::BlobImpl*)blobImpl, 16); + + path += " "; + path.AppendInt(refCount); + path += "@"; + path += addrStr; + + specialDesc = desc; + specialDesc += "\n\nNOTE: This blob (address "; + specialDesc += addrStr; + specialDesc += ") has "; + specialDesc.AppendInt(refCount); + specialDesc += " URLs."; + if (isMemoryFile) { + specialDesc += " Its size is divided "; + specialDesc += refCount > 2 ? "among" : "between"; + specialDesc += " them in this report."; + } + } + + const nsACString& descString = + specialDesc.IsEmpty() ? static_cast<const nsACString&>(desc) + : static_cast<const nsACString&>(specialDesc); + if (isMemoryFile) { + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_BYTES, + size / refCount, descString, aData); + } else { + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1, + descString, aData); + } + continue; + } + + // Just report the path for the MediaSource. + nsAutoCString path; + path = "media-source-urls/"; + BuildPath(path, key, info, aAnonymize); + + constexpr auto desc = + "An object URL allocated with URL.createObjectURL; the referenced " + "data cannot be freed until all URLs for it have been explicitly " + "invalidated with URL.revokeObjectURL."_ns; + + aCallback->Callback(""_ns, path, KIND_OTHER, UNITS_COUNT, 1, desc, aData); + } + + return NS_OK; + } + + // Initialize info->mStack to record JS stack info, if enabled. + // The string generated here is used in ReportCallback, below. + static void GetJSStackForBlob(mozilla::dom::DataInfo* aInfo) { + nsCString& stack = aInfo->mStack; + MOZ_ASSERT(stack.IsEmpty()); + const uint32_t maxFrames = + Preferences::GetUint("memory.blob_report.stack_frames"); + + if (maxFrames == 0) { + return; + } + + nsCOMPtr<nsIStackFrame> frame = dom::GetCurrentJSStack(maxFrames); + + nsAutoCString origin; + + aInfo->mPrincipal->GetPrePath(origin); + + // If we got a frame, we better have a current JSContext. This is cheating + // a bit; ideally we'd have our caller pass in a JSContext, or have + // GetCurrentJSStack() hand out the JSContext it found. + JSContext* cx = frame ? nsContentUtils::GetCurrentJSContext() : nullptr; + + while (frame) { + nsString fileNameUTF16; + frame->GetFilename(cx, fileNameUTF16); + + int32_t lineNumber = frame->GetLineNumber(cx); + + if (!fileNameUTF16.IsEmpty()) { + NS_ConvertUTF16toUTF8 fileName(fileNameUTF16); + stack += "js("; + if (!origin.IsEmpty()) { + // Make the file name root-relative for conciseness if possible. + const char* originData; + uint32_t originLen; + + originLen = origin.GetData(&originData); + // If fileName starts with origin + "/", cut up to that "/". + if (fileName.Length() >= originLen + 1 && + memcmp(fileName.get(), originData, originLen) == 0 && + fileName[originLen] == '/') { + fileName.Cut(0, originLen); + } + } + fileName.ReplaceChar('/', '\\'); + stack += fileName; + if (lineNumber > 0) { + stack += ", line="; + stack.AppendInt(lineNumber); + } + stack += ")/"; + } + + frame = frame->GetCaller(cx); + } + } + + private: + ~BlobURLsReporter() = default; + + static void BuildPath(nsAutoCString& path, nsCStringHashKey::KeyType aKey, + mozilla::dom::DataInfo* aInfo, bool anonymize) { + nsAutoCString url, owner; + aInfo->mPrincipal->GetAsciiSpec(owner); + if (!owner.IsEmpty()) { + owner.ReplaceChar('/', '\\'); + path += "owner("; + if (anonymize) { + path += "<anonymized>"; + } else { + path += owner; + } + path += ")"; + } else { + path += "owner unknown"; + } + path += "/"; + if (anonymize) { + path += "<anonymized-stack>"; + } else { + path += aInfo->mStack; + } + url = aKey; + url.ReplaceChar('/', '\\'); + if (anonymize) { + path += "<anonymized-url>"; + } else { + path += url; + } + } +}; + +NS_IMPL_ISUPPORTS(BlobURLsReporter, nsIMemoryReporter) + +class ReleasingTimerHolder final : public Runnable, + public nsITimerCallback, + public nsIAsyncShutdownBlocker { + public: + NS_DECL_ISUPPORTS_INHERITED + + static void Create(const nsACString& aURI) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ReleasingTimerHolder> holder = new ReleasingTimerHolder(aURI); + + // BlobURLProtocolHandler::RemoveDataEntry potentially happens late. We are + // prepared to RevokeUri synchronously if we run after XPCOMWillShutdown, + // but we need at least to be able to dispatch to the main thread here. + auto raii = MakeScopeExit([holder] { holder->CancelTimerAndRevokeURI(); }); + + nsresult rv = + SchedulerGroup::Dispatch(TaskCategory::Other, holder.forget()); + NS_ENSURE_SUCCESS_VOID(rv); + + raii.release(); + } + + // Runnable interface + + NS_IMETHOD + Run() override { + RefPtr<ReleasingTimerHolder> self = this; + auto raii = MakeScopeExit([self] { self->CancelTimerAndRevokeURI(); }); + + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, RELEASING_TIMER, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase(); + NS_ENSURE_TRUE(!!phase, NS_OK); + + rv = phase->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), + __LINE__, u"ReleasingTimerHolder shutdown"_ns); + NS_ENSURE_SUCCESS(rv, NS_OK); + + raii.release(); + return NS_OK; + } + + // nsITimerCallback interface + + NS_IMETHOD + Notify(nsITimer* aTimer) override { + RevokeURI(); + return NS_OK; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + using nsINamed::GetName; +#endif + + // nsIAsyncShutdownBlocker interface + + NS_IMETHOD + GetName(nsAString& aName) override { + aName.AssignLiteral("ReleasingTimerHolder for blobURL: "); + aName.Append(NS_ConvertUTF8toUTF16(mURI)); + return NS_OK; + } + + NS_IMETHOD + BlockShutdown(nsIAsyncShutdownClient* aClient) override { + CancelTimerAndRevokeURI(); + return NS_OK; + } + + NS_IMETHOD + GetState(nsIPropertyBag**) override { return NS_OK; } + + private: + explicit ReleasingTimerHolder(const nsACString& aURI) + : Runnable("ReleasingTimerHolder"), mURI(aURI) {} + + ~ReleasingTimerHolder() override = default; + + void RevokeURI() { + // Remove the shutting down blocker + nsCOMPtr<nsIAsyncShutdownClient> phase = GetShutdownPhase(); + if (phase) { + phase->RemoveBlocker(this); + } + + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = + GetDataInfo(mURI, true /* We care about revoked dataInfo */); + if (!info) { + // Already gone! + return; + } + + MOZ_ASSERT(info->mRevoked); + + StaticMutexAutoLock lock(sMutex); + gDataTable->Remove(mURI); + if (gDataTable->Count() == 0) { + delete gDataTable; + gDataTable = nullptr; + } + } + + void CancelTimerAndRevokeURI() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + + RevokeURI(); + } + + static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownPhase() { + nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService(); + NS_ENSURE_TRUE(!!svc, nullptr); + + nsCOMPtr<nsIAsyncShutdownClient> phase; + nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(phase)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return phase; + } + + nsCString mURI; + nsCOMPtr<nsITimer> mTimer; +}; + +NS_IMPL_ISUPPORTS_INHERITED(ReleasingTimerHolder, Runnable, nsITimerCallback, + nsIAsyncShutdownBlocker) + +template <typename T> +static void AddDataEntryInternal(const nsACString& aURI, T aObject, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + StaticMutexAutoLock lock(sMutex); + if (!gDataTable) { + gDataTable = new nsClassHashtable<nsCStringHashKey, mozilla::dom::DataInfo>; + } + + mozilla::UniquePtr<mozilla::dom::DataInfo> info = + mozilla::MakeUnique<mozilla::dom::DataInfo>(aObject, aPrincipal, + aAgentClusterId); + BlobURLsReporter::GetJSStackForBlob(info.get()); + + gDataTable->InsertOrUpdate(aURI, std::move(info)); +} + +void BlobURLProtocolHandler::Init(void) { + static bool initialized = false; + + if (!initialized) { + initialized = true; + RegisterStrongMemoryReporter(new BlobURLsReporter()); + } +} + +BlobURLProtocolHandler::BlobURLProtocolHandler() { Init(); } + +BlobURLProtocolHandler::~BlobURLProtocolHandler() = default; + +/* static */ +nsresult BlobURLProtocolHandler::AddDataEntry( + mozilla::dom::BlobImpl* aBlobImpl, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, nsACString& aUri) { + MOZ_ASSERT(aBlobImpl); + MOZ_ASSERT(aPrincipal); + + Init(); + + nsresult rv = GenerateURIString(aPrincipal, aUri); + NS_ENSURE_SUCCESS(rv, rv); + + AddDataEntryInternal(aUri, aBlobImpl, aPrincipal, aAgentClusterId); + + BroadcastBlobURLRegistration(aUri, aBlobImpl, aPrincipal, aAgentClusterId); + return NS_OK; +} + +/* static */ +nsresult BlobURLProtocolHandler::AddDataEntry( + MediaSource* aMediaSource, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, nsACString& aUri) { + MOZ_ASSERT(aMediaSource); + MOZ_ASSERT(aPrincipal); + + Init(); + + nsresult rv = GenerateURIString(aPrincipal, aUri); + NS_ENSURE_SUCCESS(rv, rv); + + AddDataEntryInternal(aUri, aMediaSource, aPrincipal, aAgentClusterId); + return NS_OK; +} + +/* static */ +void BlobURLProtocolHandler::AddDataEntry(const nsACString& aURI, + nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId, + mozilla::dom::BlobImpl* aBlobImpl) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aBlobImpl); + AddDataEntryInternal(aURI, aBlobImpl, aPrincipal, aAgentClusterId); +} + +/* static */ +bool BlobURLProtocolHandler::ForEachBlobURL( + std::function<bool(mozilla::dom::BlobImpl*, nsIPrincipal*, + const Maybe<nsID>&, const nsACString&, bool aRevoked)>&& + aCb) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!gDataTable) { + return false; + } + + for (const auto& entry : *gDataTable) { + mozilla::dom::DataInfo* info = entry.GetWeak(); + MOZ_ASSERT(info); + + if (info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + continue; + } + + MOZ_ASSERT(info->mBlobImpl); + if (!aCb(info->mBlobImpl, info->mPrincipal, info->mAgentClusterId, + entry.GetKey(), info->mRevoked)) { + return false; + } + } + + return true; +} + +/*static */ +void BlobURLProtocolHandler::RemoveDataEntry(const nsACString& aUri, + bool aBroadcastToOtherProcesses) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + if (!gDataTable) { + return; + } + mozilla::dom::DataInfo* info = GetDataInfo(aUri); + if (!info) { + return; + } + + { + StaticMutexAutoLock lock(sMutex); + info->mRevoked = true; + } + + if (aBroadcastToOtherProcesses && + info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + BroadcastBlobURLUnregistration(nsCString(aUri), info->mPrincipal); + } + + // The timer will take care of removing the entry for real after + // RELEASING_TIMER milliseconds. In the meantime, the mozilla::dom::DataInfo, + // marked as revoked, will not be exposed. + ReleasingTimerHolder::Create(aUri); +} + +/*static */ +bool BlobURLProtocolHandler::RemoveDataEntry( + const nsACString& aUri, nsIPrincipal* aPrincipal, + const Maybe<nsID>& aAgentClusterId) { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + if (!gDataTable) { + return false; + } + + mozilla::dom::DataInfo* info = GetDataInfo(aUri); + if (!info) { + return false; + } + + if (!aPrincipal || !aPrincipal->Subsumes(info->mPrincipal)) { + return false; + } + + if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() && + aAgentClusterId.isSome() && info->mAgentClusterId.isSome() && + !aAgentClusterId.value().Equals(info->mAgentClusterId.value())) { + return false; + } + + RemoveDataEntry(aUri, true); + return true; +} + +/* static */ +void BlobURLProtocolHandler::RemoveDataEntries() { + MOZ_ASSERT(NS_IsMainThread(), "changing gDataTable is main-thread only"); + StaticMutexAutoLock lock(sMutex); + if (!gDataTable) { + return; + } + + gDataTable->Clear(); + delete gDataTable; + gDataTable = nullptr; +} + +/* static */ +bool BlobURLProtocolHandler::HasDataEntry(const nsACString& aUri) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + return !!GetDataInfo(aUri); +} + +/* static */ +nsresult BlobURLProtocolHandler::GenerateURIString(nsIPrincipal* aPrincipal, + nsACString& aUri) { + nsresult rv; + nsCOMPtr<nsIUUIDGenerator> uuidgen = + do_GetService("@mozilla.org/uuid-generator;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + aUri.AssignLiteral(BLOBURI_SCHEME); + aUri.Append(':'); + + if (aPrincipal) { + nsAutoCString origin; + rv = aPrincipal->GetAsciiOrigin(origin); + if (NS_FAILED(rv)) { + origin.AssignLiteral("null"); + } + + aUri.Append(origin); + aUri.Append('/'); + } + + aUri += NSID_TrimBracketsASCII(id); + + return NS_OK; +} + +/* static */ +bool BlobURLProtocolHandler::GetDataEntry( + const nsACString& aUri, mozilla::dom::BlobImpl** aBlobImpl, + nsIPrincipal* aLoadingPrincipal, nsIPrincipal* aTriggeringPrincipal, + const OriginAttributes& aOriginAttributes, uint64_t aInnerWindowId, + const Maybe<nsID>& aAgentClusterId, bool aAlsoIfRevoked) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + MOZ_ASSERT(aTriggeringPrincipal); + + if (!gDataTable) { + return false; + } + + mozilla::dom::DataInfo* info = GetDataInfo(aUri, aAlsoIfRevoked); + if (!info) { + return false; + } + + // We want to be sure that we stop the creation of the channel if the blob + // URL is copy-and-pasted on a different context (ex. private browsing or + // containers). + // + // We also allow the system principal to create the channel regardless of + // the OriginAttributes. This is primarily for the benefit of mechanisms + // like the Download API that explicitly create a channel with the system + // principal and which is never mutated to have a non-zero + // mPrivateBrowsingId or container. + + if ((NS_WARN_IF(!aLoadingPrincipal) || + !aLoadingPrincipal->IsSystemPrincipal()) && + NS_WARN_IF(!ChromeUtils::IsOriginAttributesEqualIgnoringFPD( + aOriginAttributes, + BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef()))) { + return false; + } + + if (NS_WARN_IF(!aTriggeringPrincipal->Subsumes(info->mPrincipal))) { + return false; + } + + // BlobURLs are openable on the same agent-cluster-id only. + if (StaticPrefs::privacy_partition_bloburl_per_agent_cluster() && + aAgentClusterId.isSome() && info->mAgentClusterId.isSome() && + NS_WARN_IF(!aAgentClusterId->Equals(info->mAgentClusterId.value()))) { + nsAutoString localizedMsg; + AutoTArray<nsString, 1> param; + CopyUTF8toUTF16(aUri, *param.AppendElement()); + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "BlobDifferentClusterError", param, + localizedMsg); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsContentUtils::ReportToConsoleByWindowID( + localizedMsg, nsIScriptError::errorFlag, "DOM"_ns, aInnerWindowId); + return false; + } + + RefPtr<mozilla::dom::BlobImpl> blobImpl = info->mBlobImpl; + blobImpl.forget(aBlobImpl); + + return true; +} + +/* static */ +void BlobURLProtocolHandler::Traverse( + const nsACString& aUri, nsCycleCollectionTraversalCallback& aCallback) { + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + if (!gDataTable) { + return; + } + + mozilla::dom::DataInfo* res; + gDataTable->Get(aUri, &res); + if (!res) { + return; + } + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mBlobImpl"); + aCallback.NoteXPCOMChild(res->mBlobImpl); + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + aCallback, "BlobURLProtocolHandler mozilla::dom::DataInfo.mMediaSource"); + aCallback.NoteXPCOMChild(static_cast<EventTarget*>(res->mMediaSource)); +} + +NS_IMPL_ISUPPORTS(BlobURLProtocolHandler, nsIProtocolHandler, + nsISupportsWeakReference) + +/* static */ nsresult BlobURLProtocolHandler::CreateNewURI( + const nsACString& aSpec, const char* aCharset, nsIURI* aBaseURI, + nsIURI** aResult) { + *aResult = nullptr; + + // This method can be called on any thread, which is why we lock the mutex + // for read access to gDataTable. + bool revoked = true; + { + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = GetDataInfo(aSpec); + if (info && info->mObjectType == mozilla::dom::DataInfo::eBlobImpl) { + revoked = info->mRevoked; + } + } + + return NS_MutateURI(new BlobURL::Mutator()) + .SetSpec(aSpec) + .Apply(&nsIBlobURLMutator::SetRevoked, revoked) + .Finalize(aResult); +} + +NS_IMETHODIMP +BlobURLProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo, + nsIChannel** aResult) { + auto channel = MakeRefPtr<BlobURLChannel>(aURI, aLoadInfo); + channel.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +BlobURLProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + // don't override anything. + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +BlobURLProtocolHandler::GetScheme(nsACString& result) { + result.AssignLiteral(BLOBURI_SCHEME); + return NS_OK; +} + +/* static */ +bool BlobURLProtocolHandler::GetBlobURLPrincipal(nsIURI* aURI, + nsIPrincipal** aPrincipal) { + MOZ_ASSERT(aURI); + MOZ_ASSERT(aPrincipal); + + RefPtr<BlobURL> blobURL; + nsresult rv = + aURI->QueryInterface(kHOSTOBJECTURICID, getter_AddRefs(blobURL)); + if (NS_FAILED(rv) || !blobURL) { + return false; + } + + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = + GetDataInfoFromURI(aURI, true /*aAlsoIfRevoked */); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl || + !info->mBlobImpl) { + return false; + } + + nsCOMPtr<nsIPrincipal> principal; + + if (blobURL->Revoked()) { + principal = NullPrincipal::Create( + BasePrincipal::Cast(info->mPrincipal)->OriginAttributesRef()); + } else { + principal = info->mPrincipal; + } + + principal.forget(aPrincipal); + return true; +} + +bool BlobURLProtocolHandler::IsBlobURLBroadcastPrincipal( + nsIPrincipal* aPrincipal) { + return aPrincipal->IsSystemPrincipal() || + aPrincipal->GetIsAddonOrExpandedAddonPrincipal(); +} + +} // namespace dom +} // namespace mozilla + +nsresult NS_GetBlobForBlobURI(nsIURI* aURI, mozilla::dom::BlobImpl** aBlob) { + *aBlob = nullptr; + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = + mozilla::dom::GetDataInfoFromURI(aURI, false /* aAlsoIfRevoked */); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl; + blob.forget(aBlob); + return NS_OK; +} + +nsresult NS_GetBlobForBlobURISpec(const nsACString& aSpec, + mozilla::dom::BlobImpl** aBlob, + bool aAlsoIfRevoked) { + *aBlob = nullptr; + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + + mozilla::dom::DataInfo* info = + mozilla::dom::GetDataInfo(aSpec, aAlsoIfRevoked); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eBlobImpl || + !info->mBlobImpl) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::BlobImpl> blob = info->mBlobImpl; + blob.forget(aBlob); + return NS_OK; +} + +nsresult NS_GetSourceForMediaSourceURI(nsIURI* aURI, + mozilla::dom::MediaSource** aSource) { + *aSource = nullptr; + + MOZ_ASSERT(NS_IsMainThread(), + "without locking gDataTable is main-thread only"); + mozilla::dom::DataInfo* info = mozilla::dom::GetDataInfoFromURI(aURI); + if (!info || info->mObjectType != mozilla::dom::DataInfo::eMediaSource) { + return NS_ERROR_DOM_BAD_URI; + } + + RefPtr<mozilla::dom::MediaSource> mediaSource = info->mMediaSource; + mediaSource.forget(aSource); + return NS_OK; +} + +namespace mozilla::dom { + +bool IsType(nsIURI* aUri, mozilla::dom::DataInfo::ObjectType aType) { + // We lock because this may be called off-main-thread + StaticMutexAutoLock lock(sMutex); + mozilla::dom::DataInfo* info = GetDataInfoFromURI(aUri); + if (!info) { + return false; + } + + return info->mObjectType == aType; +} + +bool IsBlobURI(nsIURI* aUri) { + return IsType(aUri, mozilla::dom::DataInfo::eBlobImpl); +} + +bool BlobURLSchemeIsHTTPOrHTTPS(const nsACString& aUri) { + return (StringBeginsWith(aUri, "blob:http://"_ns) || + StringBeginsWith(aUri, "blob:https://"_ns)); +} + +bool IsMediaSourceURI(nsIURI* aUri) { + return IsType(aUri, mozilla::dom::DataInfo::eMediaSource); +} + +} // namespace mozilla::dom |