diff options
Diffstat (limited to 'dom/file/uri/BlobURLInputStream.cpp')
-rw-r--r-- | dom/file/uri/BlobURLInputStream.cpp | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/dom/file/uri/BlobURLInputStream.cpp b/dom/file/uri/BlobURLInputStream.cpp new file mode 100644 index 0000000000..7610281afc --- /dev/null +++ b/dom/file/uri/BlobURLInputStream.cpp @@ -0,0 +1,589 @@ +/* -*- 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 "BlobURLInputStream.h" +#include "BlobURL.h" +#include "BlobURLChannel.h" +#include "BlobURLProtocolHandler.h" + +#include "mozilla/ScopeExit.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "nsStreamUtils.h" +#include "nsMimeTypes.h" + +namespace mozilla::dom { + +NS_IMPL_ADDREF(BlobURLInputStream); +NS_IMPL_RELEASE(BlobURLInputStream); + +NS_INTERFACE_MAP_BEGIN(BlobURLInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength) + NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAsyncInputStream) +NS_INTERFACE_MAP_END + +/* static */ +already_AddRefed<nsIInputStream> BlobURLInputStream::Create( + BlobURLChannel* const aChannel, BlobURL* const aBlobURL) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_WARN_IF(!aChannel) || NS_WARN_IF(!aBlobURL)) { + return nullptr; + } + + nsAutoCString spec; + + nsresult rv = aBlobURL->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return MakeAndAddRef<BlobURLInputStream>(aChannel, spec); +} + +// from nsIInputStream interface +NS_IMETHODIMP BlobURLInputStream::Close() { + return CloseWithStatus(NS_BASE_STREAM_CLOSED); +} + +NS_IMETHODIMP BlobURLInputStream::Available(uint64_t* aLength) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + return mAsyncInputStream->Available(aLength); + } + + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::StreamStatus() { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + return mAsyncInputStream->StreamStatus(); + } + + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::Read(char* aBuffer, uint32_t aCount, + uint32_t* aReadCount) { + MutexAutoLock lock(mStateMachineMutex); + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + // Read() should not return NS_BASE_STREAM_CLOSED if stream is closed. + // A read count of 0 should indicate closed or consumed stream. + // See: + // https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/xpcom/io/nsIInputStream.idl#104 + if (mState == State::CLOSED) { + *aReadCount = 0; + return NS_OK; + } + + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + nsresult rv = mAsyncInputStream->Read(aBuffer, aCount, aReadCount); + if (NS_SUCCEEDED(rv) && aReadCount && !*aReadCount) { + mState = State::CLOSED; + ReleaseUnderlyingStream(lock); + } + return rv; + } + + return NS_BASE_STREAM_WOULD_BLOCK; +} + +NS_IMETHODIMP BlobURLInputStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, uint32_t aCount, + uint32_t* aResult) { + // This means the caller will have to wrap the stream in an + // nsBufferedInputStream in order to use ReadSegments + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP BlobURLInputStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = true; + return NS_OK; +} + +// from nsIAsyncInputStream interface +NS_IMETHODIMP BlobURLInputStream::CloseWithStatus(nsresult aStatus) { + MutexAutoLock lock(mStateMachineMutex); + if (mState == State::READY) { + MOZ_ASSERT(mAsyncInputStream); + mAsyncInputStream->CloseWithStatus(aStatus); + } + + mState = State::CLOSED; + ReleaseUnderlyingStream(lock); + return NS_OK; +} + +NS_IMETHODIMP BlobURLInputStream::AsyncWait(nsIInputStreamCallback* aCallback, + uint32_t aFlags, + uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return NS_ERROR_FAILURE; + } + + // Pre-empting a valid callback with another is not allowed. + if (NS_WARN_IF(mAsyncWaitCallback && aCallback && + mAsyncWaitCallback != aCallback)) { + return NS_ERROR_FAILURE; + } + + mAsyncWaitTarget = aEventTarget; + mAsyncWaitRequestedCount = aRequestedCount; + mAsyncWaitFlags = aFlags; + mAsyncWaitCallback = aCallback; + + if (mState == State::INITIAL) { + mState = State::WAITING; + // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds + // or fails + if (NS_IsMainThread()) { + RetrieveBlobData(lock); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod( + "BlobURLInputStream::CallRetrieveBlobData", this, + &BlobURLInputStream::CallRetrieveBlobData); + NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL); + return NS_OK; + } + + if (mState == State::WAITING) { + // RetrieveBlobData is already in progress and will execute + // NotifyWaitTargets when retrieve succeeds or fails + return NS_OK; + } + + if (mState == State::READY) { + // Ask the blob's input stream if reading is possible or not + return mAsyncInputStream->AsyncWait( + mAsyncWaitCallback ? this : nullptr, mAsyncWaitFlags, + mAsyncWaitRequestedCount, mAsyncWaitTarget); + } + + MOZ_ASSERT(mState == State::CLOSED); + NotifyWaitTargets(lock); + return NS_OK; +} + +// from nsIInputStreamLength interface +NS_IMETHODIMP BlobURLInputStream::Length(int64_t* aLength) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::CLOSED) { + return NS_BASE_STREAM_CLOSED; + } + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return NS_ERROR_FAILURE; + } + + if (mState == State::READY) { + *aLength = mBlobSize; + return NS_OK; + } + return NS_BASE_STREAM_WOULD_BLOCK; +} + +// from nsIAsyncInputStreamLength interface +NS_IMETHODIMP BlobURLInputStream::AsyncLengthWait( + nsIInputStreamLengthCallback* aCallback, nsIEventTarget* aEventTarget) { + MutexAutoLock lock(mStateMachineMutex); + + if (mState == State::ERROR) { + MOZ_ASSERT(NS_FAILED(mError)); + return mError; + } + + // Pre-empting a valid callback with another is not allowed. + if (mAsyncLengthWaitCallback && aCallback) { + return NS_ERROR_FAILURE; + } + + mAsyncLengthWaitTarget = aEventTarget; + mAsyncLengthWaitCallback = aCallback; + + if (mState == State::INITIAL) { + mState = State::WAITING; + // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds + // or fails + if (NS_IsMainThread()) { + RetrieveBlobData(lock); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod( + "BlobURLInputStream::CallRetrieveBlobData", this, + &BlobURLInputStream::CallRetrieveBlobData); + NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL); + return NS_OK; + } + + if (mState == State::WAITING) { + // RetrieveBlobData is already in progress and will execute + // NotifyWaitTargets when retrieve succeeds or fails + return NS_OK; + } + + // Since here the state must be READY (in which case the size of the blob is + // already known) or CLOSED, callback can be called immediately + NotifyWaitTargets(lock); + return NS_OK; +} + +// from nsIInputStreamCallback interface +NS_IMETHODIMP BlobURLInputStream::OnInputStreamReady( + nsIAsyncInputStream* aStream) { + nsCOMPtr<nsIInputStreamCallback> callback; + + { + MutexAutoLock lock(mStateMachineMutex); + MOZ_ASSERT_IF(mAsyncInputStream, aStream == mAsyncInputStream); + + // aborted in the meantime + if (!mAsyncWaitCallback) { + return NS_OK; + } + + mAsyncWaitCallback.swap(callback); + mAsyncWaitTarget = nullptr; + } + + MOZ_ASSERT(callback); + return callback->OnInputStreamReady(this); +} + +// from nsIInputStreamLengthCallback interface +NS_IMETHODIMP BlobURLInputStream::OnInputStreamLengthReady( + nsIAsyncInputStreamLength* aStream, int64_t aLength) { + nsCOMPtr<nsIInputStreamLengthCallback> callback; + { + MutexAutoLock lock(mStateMachineMutex); + + // aborted in the meantime + if (!mAsyncLengthWaitCallback) { + return NS_OK; + } + + mAsyncLengthWaitCallback.swap(callback); + mAsyncLengthWaitCallback = nullptr; + } + + return callback->OnInputStreamLengthReady(this, aLength); +} + +// private: +BlobURLInputStream::~BlobURLInputStream() { + if (mChannel) { + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget()); + } +} + +BlobURLInputStream::BlobURLInputStream(BlobURLChannel* const aChannel, + nsACString& aBlobURLSpec) + : mChannel(aChannel), + mBlobURLSpec(std::move(aBlobURLSpec)), + mStateMachineMutex("BlobURLInputStream::mStateMachineMutex"), + mState(State::INITIAL), + mError(NS_OK), + mBlobSize(-1), + mAsyncWaitFlags(), + mAsyncWaitRequestedCount() {} + +void BlobURLInputStream::WaitOnUnderlyingStream( + const MutexAutoLock& aProofOfLock) { + if (mAsyncWaitCallback || mAsyncWaitTarget) { + // AsyncWait should be called on the underlying stream + mAsyncInputStream->AsyncWait(mAsyncWaitCallback ? this : nullptr, + mAsyncWaitFlags, mAsyncWaitRequestedCount, + mAsyncWaitTarget); + } + + if (mAsyncLengthWaitCallback || mAsyncLengthWaitTarget) { + // AsyncLengthWait should be called on the underlying stream + nsCOMPtr<nsIAsyncInputStreamLength> asyncStreamLength = + do_QueryInterface(mAsyncInputStream); + if (asyncStreamLength) { + asyncStreamLength->AsyncLengthWait( + mAsyncLengthWaitCallback ? this : nullptr, mAsyncLengthWaitTarget); + } + } +} + +void BlobURLInputStream::CallRetrieveBlobData() { + MutexAutoLock lock(mStateMachineMutex); + RetrieveBlobData(lock); +} + +void BlobURLInputStream::RetrieveBlobData(const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + + MOZ_ASSERT(mState == State::WAITING); + + auto cleanupOnEarlyExit = MakeScopeExit([&] { + mState = State::ERROR; + mError = NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget()); + NotifyWaitTargets(aProofOfLock); + }); + + nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo(); + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + nsCOMPtr<nsIPrincipal> loadingPrincipal; + if (NS_WARN_IF(NS_FAILED(loadInfo->GetTriggeringPrincipal( + getter_AddRefs(triggeringPrincipal)))) || + NS_WARN_IF(!triggeringPrincipal)) { + NS_WARNING("Failed to get owning channel's triggering principal"); + return; + } + + if (NS_WARN_IF(NS_FAILED( + loadInfo->GetLoadingPrincipal(getter_AddRefs(loadingPrincipal))))) { + NS_WARNING("Failed to get owning channel's loading principal"); + return; + } + + Maybe<nsID> agentClusterId; + Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo(); + if (clientInfo.isSome()) { + agentClusterId = clientInfo->AgentClusterId(); + } + + if (XRE_IsParentProcess() || !BlobURLSchemeIsHTTPOrHTTPS(mBlobURLSpec)) { + RefPtr<BlobImpl> blobImpl; + + // Since revoked blobs are also retrieved, it is possible that the blob no + // longer exists (due to the 5 second timeout) when execution reaches here + if (!BlobURLProtocolHandler::GetDataEntry( + mBlobURLSpec, getter_AddRefs(blobImpl), loadingPrincipal, + triggeringPrincipal, loadInfo->GetOriginAttributes(), + loadInfo->GetInnerWindowID(), agentClusterId, + true /* AlsoIfRevoked */)) { + NS_WARNING("Failed to get data entry principal. URL revoked?"); + return; + } + + if (NS_WARN_IF( + NS_FAILED(StoreBlobImplStream(blobImpl.forget(), aProofOfLock)))) { + return; + } + + mState = State::READY; + + // By design, execution can only reach here when a caller has called + // AsyncWait or AsyncLengthWait on this stream. The underlying stream is + // valid, but the caller should not be informed until that stream has data + // to read or it is closed. + WaitOnUnderlyingStream(aProofOfLock); + + cleanupOnEarlyExit.release(); + return; + } + + ContentChild* contentChild{ContentChild::GetSingleton()}; + MOZ_ASSERT(contentChild); + + const RefPtr<BlobURLInputStream> self = this; + + cleanupOnEarlyExit.release(); + + contentChild + ->SendBlobURLDataRequest(mBlobURLSpec, triggeringPrincipal, + loadingPrincipal, + loadInfo->GetOriginAttributes(), + loadInfo->GetInnerWindowID(), agentClusterId) + ->Then( + GetCurrentSerialEventTarget(), __func__, + [self](const BlobURLDataRequestResult& aResult) { + MutexAutoLock lock(self->mStateMachineMutex); + if (aResult.type() == BlobURLDataRequestResult::TIPCBlob) { + if (self->mState == State::WAITING) { + RefPtr<BlobImpl> blobImpl = + IPCBlobUtils::Deserialize(aResult.get_IPCBlob()); + if (blobImpl && self->StoreBlobImplStream(blobImpl.forget(), + lock) == NS_OK) { + self->mState = State::READY; + // By design, execution can only reach here when a caller has + // called AsyncWait or AsyncLengthWait on this stream. The + // underlying stream is valid, but the caller should not be + // informed until that stream has data to read or it is + // closed. + self->WaitOnUnderlyingStream(lock); + return; + } + } else { + MOZ_ASSERT(self->mState == State::CLOSED); + // Callback can be called immediately + self->NotifyWaitTargets(lock); + return; + } + } + NS_WARNING("Blob data was not retrieved!"); + self->mState = State::ERROR; + self->mError = aResult.type() == BlobURLDataRequestResult::Tnsresult + ? aResult.get_nsresult() + : NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", + self->mChannel.forget()); + self->NotifyWaitTargets(lock); + }, + [self](mozilla::ipc::ResponseRejectReason aReason) { + MutexAutoLock lock(self->mStateMachineMutex); + NS_WARNING("IPC call to SendBlobURLDataRequest failed!"); + self->mState = State::ERROR; + self->mError = NS_ERROR_FAILURE; + NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", + self->mChannel.forget()); + self->NotifyWaitTargets(lock); + }); +} + +nsresult BlobURLInputStream::StoreBlobImplStream( + already_AddRefed<BlobImpl> aBlobImpl, const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); + const RefPtr<BlobImpl> blobImpl = aBlobImpl; + nsAutoString blobContentType; + nsAutoCString channelContentType; + + blobImpl->GetType(blobContentType); + mChannel->GetContentType(channelContentType); + // A empty content type is the correct channel content type in the case of a + // fetch of a blob where the type was not set. It is invalid in others cases + // such as a XHR (See https://xhr.spec.whatwg.org/#response-mime-type). The + // XMLHttpRequestMainThread will set the channel content type to the correct + // fallback value before this point, so we need to be careful to only override + // it when the blob type is valid. + if (!blobContentType.IsEmpty() || + channelContentType.EqualsLiteral(UNKNOWN_CONTENT_TYPE)) { + mChannel->SetContentType(NS_ConvertUTF16toUTF8(blobContentType)); + } + + auto cleanupOnExit = MakeScopeExit([&] { mChannel = nullptr; }); + + if (blobImpl->IsFile()) { + nsAutoString filename; + blobImpl->GetName(filename); + + // Don't overwrite existing name. + nsString ignored; + bool hasName = + NS_SUCCEEDED(mChannel->GetContentDispositionFilename(ignored)); + + if (!filename.IsEmpty() && !hasName) { + mChannel->SetContentDispositionFilename(filename); + } + } + + mozilla::ErrorResult errorResult; + + mBlobSize = blobImpl->GetSize(errorResult); + + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + mChannel->SetContentLength(mBlobSize); + + nsCOMPtr<nsIInputStream> inputStream; + blobImpl->CreateInputStream(getter_AddRefs(inputStream), errorResult); + + if (NS_WARN_IF(errorResult.Failed())) { + return errorResult.StealNSResult(); + } + + if (NS_WARN_IF(!inputStream)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = NS_MakeAsyncNonBlockingInputStream( + inputStream.forget(), getter_AddRefs(mAsyncInputStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!mAsyncInputStream)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +void BlobURLInputStream::NotifyWaitTargets(const MutexAutoLock& aProofOfLock) { + if (mAsyncWaitCallback) { + auto callback = mAsyncWaitTarget + ? NS_NewInputStreamReadyEvent( + "BlobURLInputStream::OnInputStreamReady", + mAsyncWaitCallback, mAsyncWaitTarget) + : mAsyncWaitCallback; + + mAsyncWaitCallback = nullptr; + mAsyncWaitTarget = nullptr; + callback->OnInputStreamReady(this); + } + + if (mAsyncLengthWaitCallback) { + const RefPtr<BlobURLInputStream> self = this; + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "BlobURLInputStream::OnInputStreamLengthReady", [self] { + self->mAsyncLengthWaitCallback->OnInputStreamLengthReady( + self, self->mBlobSize); + }); + + mAsyncLengthWaitCallback = nullptr; + + if (mAsyncLengthWaitTarget) { + mAsyncLengthWaitTarget->Dispatch(runnable, NS_DISPATCH_NORMAL); + mAsyncLengthWaitTarget = nullptr; + } else { + runnable->Run(); + } + } +} + +void BlobURLInputStream::ReleaseUnderlyingStream( + const MutexAutoLock& aProofOfLock) { + mAsyncInputStream = nullptr; + mBlobSize = -1; +} + +} // namespace mozilla::dom |