1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "BlobURLInputStream.h"
8 #include "BlobURL.h"
9 #include "BlobURLChannel.h"
10 #include "BlobURLProtocolHandler.h"
11 
12 #include "mozilla/ScopeExit.h"
13 #include "mozilla/dom/ContentChild.h"
14 #include "mozilla/dom/IPCBlobUtils.h"
15 #include "nsStreamUtils.h"
16 
17 namespace mozilla::dom {
18 
19 NS_IMPL_ADDREF(BlobURLInputStream);
20 NS_IMPL_RELEASE(BlobURLInputStream);
21 
22 NS_INTERFACE_MAP_BEGIN(BlobURLInputStream)
NS_INTERFACE_MAP_ENTRY(nsIInputStream)23   NS_INTERFACE_MAP_ENTRY(nsIInputStream)
24   NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream)
25   NS_INTERFACE_MAP_ENTRY(nsIInputStreamLength)
26   NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStreamLength)
27   NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
28   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAsyncInputStream)
29 NS_INTERFACE_MAP_END
30 
31 /* static */
32 already_AddRefed<nsIInputStream> BlobURLInputStream::Create(
33     BlobURLChannel* const aChannel, BlobURL* const aBlobURL) {
34   MOZ_ASSERT(NS_IsMainThread());
35 
36   if (NS_WARN_IF(!aChannel) || NS_WARN_IF(!aBlobURL)) {
37     return nullptr;
38   }
39 
40   nsAutoCString spec;
41 
42   nsresult rv = aBlobURL->GetSpec(spec);
43   if (NS_WARN_IF(NS_FAILED(rv))) {
44     return nullptr;
45   }
46 
47   return MakeAndAddRef<BlobURLInputStream>(aChannel, spec);
48 }
49 
50 // from nsIInputStream interface
Close()51 NS_IMETHODIMP BlobURLInputStream::Close() {
52   return CloseWithStatus(NS_BASE_STREAM_CLOSED);
53 }
54 
Available(uint64_t * aLength)55 NS_IMETHODIMP BlobURLInputStream::Available(uint64_t* aLength) {
56   MutexAutoLock lock(mStateMachineMutex);
57 
58   if (mState == State::ERROR) {
59     MOZ_ASSERT(NS_FAILED(mError));
60     return mError;
61   }
62 
63   if (mState == State::CLOSED) {
64     return NS_BASE_STREAM_CLOSED;
65   }
66 
67   if (mState == State::READY) {
68     MOZ_ASSERT(mAsyncInputStream);
69     return mAsyncInputStream->Available(aLength);
70   }
71 
72   return NS_BASE_STREAM_WOULD_BLOCK;
73 }
74 
Read(char * aBuffer,uint32_t aCount,uint32_t * aReadCount)75 NS_IMETHODIMP BlobURLInputStream::Read(char* aBuffer, uint32_t aCount,
76                                        uint32_t* aReadCount) {
77   MutexAutoLock lock(mStateMachineMutex);
78   if (mState == State::ERROR) {
79     MOZ_ASSERT(NS_FAILED(mError));
80     return mError;
81   }
82 
83   // Read() should not return NS_BASE_STREAM_CLOSED if stream is closed.
84   // A read count of 0 should indicate closed or consumed stream.
85   // See:
86   // https://searchfox.org/mozilla-central/rev/559b25eb41c1cbffcb90a34e008b8288312fcd25/xpcom/io/nsIInputStream.idl#104
87   if (mState == State::CLOSED) {
88     *aReadCount = 0;
89     return NS_OK;
90   }
91 
92   if (mState == State::READY) {
93     MOZ_ASSERT(mAsyncInputStream);
94     nsresult rv = mAsyncInputStream->Read(aBuffer, aCount, aReadCount);
95     if (NS_SUCCEEDED(rv) && aReadCount && !*aReadCount) {
96       mState = State::CLOSED;
97       ReleaseUnderlyingStream(lock);
98     }
99     return rv;
100   }
101 
102   return NS_BASE_STREAM_WOULD_BLOCK;
103 }
104 
ReadSegments(nsWriteSegmentFun aWriter,void * aClosure,uint32_t aCount,uint32_t * aResult)105 NS_IMETHODIMP BlobURLInputStream::ReadSegments(nsWriteSegmentFun aWriter,
106                                                void* aClosure, uint32_t aCount,
107                                                uint32_t* aResult) {
108   // This means the caller will have to wrap the stream in an
109   // nsBufferedInputStream in order to use ReadSegments
110   return NS_ERROR_NOT_IMPLEMENTED;
111 }
112 
IsNonBlocking(bool * aNonBlocking)113 NS_IMETHODIMP BlobURLInputStream::IsNonBlocking(bool* aNonBlocking) {
114   *aNonBlocking = true;
115   return NS_OK;
116 }
117 
118 // from nsIAsyncInputStream interface
CloseWithStatus(nsresult aStatus)119 NS_IMETHODIMP BlobURLInputStream::CloseWithStatus(nsresult aStatus) {
120   MutexAutoLock lock(mStateMachineMutex);
121   if (mState == State::READY) {
122     MOZ_ASSERT(mAsyncInputStream);
123     mAsyncInputStream->CloseWithStatus(aStatus);
124   }
125 
126   mState = State::CLOSED;
127   ReleaseUnderlyingStream(lock);
128   return NS_OK;
129 }
130 
AsyncWait(nsIInputStreamCallback * aCallback,uint32_t aFlags,uint32_t aRequestedCount,nsIEventTarget * aEventTarget)131 NS_IMETHODIMP BlobURLInputStream::AsyncWait(nsIInputStreamCallback* aCallback,
132                                             uint32_t aFlags,
133                                             uint32_t aRequestedCount,
134                                             nsIEventTarget* aEventTarget) {
135   MutexAutoLock lock(mStateMachineMutex);
136 
137   if (mState == State::ERROR) {
138     MOZ_ASSERT(NS_FAILED(mError));
139     return NS_ERROR_FAILURE;
140   }
141 
142   // Pre-empting a valid callback with another is not allowed.
143   if (NS_WARN_IF(mAsyncWaitCallback && aCallback)) {
144     return NS_ERROR_FAILURE;
145   }
146 
147   mAsyncWaitTarget = aEventTarget;
148   mAsyncWaitRequestedCount = aRequestedCount;
149   mAsyncWaitFlags = aFlags;
150   mAsyncWaitCallback = aCallback;
151 
152   if (mState == State::INITIAL) {
153     mState = State::WAITING;
154     // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds
155     // or fails
156     if (NS_IsMainThread()) {
157       RetrieveBlobData(lock);
158       return NS_OK;
159     }
160 
161     nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod(
162         "BlobURLInputStream::CallRetrieveBlobData", this,
163         &BlobURLInputStream::CallRetrieveBlobData);
164     NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL);
165     return NS_OK;
166   }
167 
168   if (mState == State::WAITING) {
169     // RetrieveBlobData is already in progress and will execute
170     // NotifyWaitTargets when retrieve succeeds or fails
171     return NS_OK;
172   }
173 
174   if (mState == State::READY) {
175     // Ask the blob's input stream if reading is possible or not
176     return mAsyncInputStream->AsyncWait(
177         mAsyncWaitCallback ? this : nullptr, mAsyncWaitFlags,
178         mAsyncWaitRequestedCount, mAsyncWaitTarget);
179   }
180 
181   MOZ_ASSERT(mState == State::CLOSED);
182   NotifyWaitTargets(lock);
183   return NS_OK;
184 }
185 
186 // from nsIInputStreamLength interface
Length(int64_t * aLength)187 NS_IMETHODIMP BlobURLInputStream::Length(int64_t* aLength) {
188   MutexAutoLock lock(mStateMachineMutex);
189 
190   if (mState == State::CLOSED) {
191     return NS_BASE_STREAM_CLOSED;
192   }
193 
194   if (mState == State::ERROR) {
195     MOZ_ASSERT(NS_FAILED(mError));
196     return NS_ERROR_FAILURE;
197   }
198 
199   if (mState == State::READY) {
200     *aLength = mBlobSize;
201     return NS_OK;
202   }
203   return NS_BASE_STREAM_WOULD_BLOCK;
204 }
205 
206 // from nsIAsyncInputStreamLength interface
AsyncLengthWait(nsIInputStreamLengthCallback * aCallback,nsIEventTarget * aEventTarget)207 NS_IMETHODIMP BlobURLInputStream::AsyncLengthWait(
208     nsIInputStreamLengthCallback* aCallback, nsIEventTarget* aEventTarget) {
209   MutexAutoLock lock(mStateMachineMutex);
210 
211   if (mState == State::ERROR) {
212     MOZ_ASSERT(NS_FAILED(mError));
213     return mError;
214   }
215 
216   // Pre-empting a valid callback with another is not allowed.
217   if (mAsyncLengthWaitCallback && aCallback) {
218     return NS_ERROR_FAILURE;
219   }
220 
221   mAsyncLengthWaitTarget = aEventTarget;
222   mAsyncLengthWaitCallback = aCallback;
223 
224   if (mState == State::INITIAL) {
225     mState = State::WAITING;
226     // RetrieveBlobData will execute NotifyWWaitTarget() when retrieve succeeds
227     // or fails
228     if (NS_IsMainThread()) {
229       RetrieveBlobData(lock);
230       return NS_OK;
231     }
232 
233     nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod(
234         "BlobURLInputStream::CallRetrieveBlobData", this,
235         &BlobURLInputStream::CallRetrieveBlobData);
236     NS_DispatchToMainThread(runnable.forget(), NS_DISPATCH_NORMAL);
237     return NS_OK;
238   }
239 
240   if (mState == State::WAITING) {
241     // RetrieveBlobData is already in progress and will execute
242     // NotifyWaitTargets when retrieve succeeds or fails
243     return NS_OK;
244   }
245 
246   // Since here the state must be READY (in which case the size of the blob is
247   // already known) or CLOSED, callback can be called immediately
248   NotifyWaitTargets(lock);
249   return NS_OK;
250 }
251 
252 // from nsIInputStreamCallback interface
OnInputStreamReady(nsIAsyncInputStream * aStream)253 NS_IMETHODIMP BlobURLInputStream::OnInputStreamReady(
254     nsIAsyncInputStream* aStream) {
255   nsCOMPtr<nsIInputStreamCallback> callback;
256 
257   {
258     MutexAutoLock lock(mStateMachineMutex);
259     MOZ_ASSERT_IF(mAsyncInputStream, aStream == mAsyncInputStream);
260 
261     // aborted in the meantime
262     if (!mAsyncWaitCallback) {
263       return NS_OK;
264     }
265 
266     mAsyncWaitCallback.swap(callback);
267     mAsyncWaitTarget = nullptr;
268   }
269 
270   MOZ_ASSERT(callback);
271   return callback->OnInputStreamReady(this);
272 }
273 
274 // from nsIInputStreamLengthCallback interface
OnInputStreamLengthReady(nsIAsyncInputStreamLength * aStream,int64_t aLength)275 NS_IMETHODIMP BlobURLInputStream::OnInputStreamLengthReady(
276     nsIAsyncInputStreamLength* aStream, int64_t aLength) {
277   nsCOMPtr<nsIInputStreamLengthCallback> callback;
278   {
279     MutexAutoLock lock(mStateMachineMutex);
280 
281     // aborted in the meantime
282     if (!mAsyncLengthWaitCallback) {
283       return NS_OK;
284     }
285 
286     mAsyncLengthWaitCallback.swap(callback);
287     mAsyncLengthWaitCallback = nullptr;
288   }
289 
290   return callback->OnInputStreamLengthReady(this, aLength);
291 }
292 
293 // private:
~BlobURLInputStream()294 BlobURLInputStream::~BlobURLInputStream() {
295   if (mChannel) {
296     NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget());
297   }
298 }
299 
BlobURLInputStream(BlobURLChannel * const aChannel,nsACString & aBlobURLSpec)300 BlobURLInputStream::BlobURLInputStream(BlobURLChannel* const aChannel,
301                                        nsACString& aBlobURLSpec)
302     : mChannel(aChannel),
303       mBlobURLSpec(std::move(aBlobURLSpec)),
304       mStateMachineMutex("BlobURLInputStream::mStateMachineMutex"),
305       mState(State::INITIAL),
306       mError(NS_OK),
307       mBlobSize(-1),
308       mAsyncWaitFlags(),
309       mAsyncWaitRequestedCount() {}
310 
WaitOnUnderlyingStream(const MutexAutoLock & aProofOfLock)311 void BlobURLInputStream::WaitOnUnderlyingStream(
312     const MutexAutoLock& aProofOfLock) {
313   if (mAsyncWaitCallback || mAsyncWaitTarget) {
314     // AsyncWait should be called on the underlying stream
315     mAsyncInputStream->AsyncWait(mAsyncWaitCallback ? this : nullptr,
316                                  mAsyncWaitFlags, mAsyncWaitRequestedCount,
317                                  mAsyncWaitTarget);
318   }
319 
320   if (mAsyncLengthWaitCallback || mAsyncLengthWaitTarget) {
321     // AsyncLengthWait should be called on the underlying stream
322     nsCOMPtr<nsIAsyncInputStreamLength> asyncStreamLength =
323         do_QueryInterface(mAsyncInputStream);
324     if (asyncStreamLength) {
325       asyncStreamLength->AsyncLengthWait(
326           mAsyncLengthWaitCallback ? this : nullptr, mAsyncLengthWaitTarget);
327     }
328   }
329 }
330 
CallRetrieveBlobData()331 void BlobURLInputStream::CallRetrieveBlobData() {
332   MutexAutoLock lock(mStateMachineMutex);
333   RetrieveBlobData(lock);
334 }
335 
RetrieveBlobData(const MutexAutoLock & aProofOfLock)336 void BlobURLInputStream::RetrieveBlobData(const MutexAutoLock& aProofOfLock) {
337   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
338 
339   MOZ_ASSERT(mState == State::WAITING);
340 
341   auto cleanupOnEarlyExit = MakeScopeExit([&] {
342     mState = State::ERROR;
343     mError = NS_ERROR_FAILURE;
344     NS_ReleaseOnMainThread("BlobURLInputStream::mChannel", mChannel.forget());
345     NotifyWaitTargets(aProofOfLock);
346   });
347 
348   nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
349   nsCOMPtr<nsIPrincipal> triggeringPrincipal;
350   nsCOMPtr<nsIPrincipal> loadingPrincipal;
351   if (NS_WARN_IF(NS_FAILED(loadInfo->GetTriggeringPrincipal(
352           getter_AddRefs(triggeringPrincipal)))) ||
353       NS_WARN_IF(!triggeringPrincipal)) {
354     NS_WARNING("Failed to get owning channel's triggering principal");
355     return;
356   }
357 
358   if (NS_WARN_IF(NS_FAILED(
359           loadInfo->GetLoadingPrincipal(getter_AddRefs(loadingPrincipal))))) {
360     NS_WARNING("Failed to get owning channel's loading principal");
361     return;
362   }
363 
364   Maybe<nsID> agentClusterId;
365   Maybe<ClientInfo> clientInfo = loadInfo->GetClientInfo();
366   if (clientInfo.isSome()) {
367     agentClusterId = clientInfo->AgentClusterId();
368   }
369 
370   if (XRE_IsParentProcess() || !BlobURLSchemeIsHTTPOrHTTPS(mBlobURLSpec)) {
371     RefPtr<BlobImpl> blobImpl;
372 
373     // Since revoked blobs are also retrieved, it is possible that the blob no
374     // longer exists (due to the 5 second timeout) when execution reaches here
375     if (!BlobURLProtocolHandler::GetDataEntry(
376             mBlobURLSpec, getter_AddRefs(blobImpl), loadingPrincipal,
377             triggeringPrincipal, loadInfo->GetOriginAttributes(),
378             loadInfo->GetInnerWindowID(), agentClusterId,
379             true /* AlsoIfRevoked */)) {
380       NS_WARNING("Failed to get data entry principal. URL revoked?");
381       return;
382     }
383 
384     if (NS_WARN_IF(
385             NS_FAILED(StoreBlobImplStream(blobImpl.forget(), aProofOfLock)))) {
386       return;
387     }
388 
389     mState = State::READY;
390 
391     // By design, execution can only reach here when a caller has called
392     // AsyncWait or AsyncLengthWait on this stream. The underlying stream is
393     // valid, but the caller should not be informed until that stream has data
394     // to read or it is closed.
395     WaitOnUnderlyingStream(aProofOfLock);
396 
397     cleanupOnEarlyExit.release();
398     return;
399   }
400 
401   ContentChild* contentChild{ContentChild::GetSingleton()};
402   MOZ_ASSERT(contentChild);
403 
404   const RefPtr<BlobURLInputStream> self = this;
405 
406   cleanupOnEarlyExit.release();
407 
408   contentChild
409       ->SendBlobURLDataRequest(mBlobURLSpec, triggeringPrincipal,
410                                loadingPrincipal,
411                                loadInfo->GetOriginAttributes(),
412                                loadInfo->GetInnerWindowID(), agentClusterId)
413       ->Then(
414           GetCurrentSerialEventTarget(), __func__,
415           [self](const BlobURLDataRequestResult& aResult) {
416             MutexAutoLock lock(self->mStateMachineMutex);
417             if (aResult.type() == BlobURLDataRequestResult::TIPCBlob) {
418               if (self->mState == State::WAITING) {
419                 RefPtr<BlobImpl> blobImpl =
420                     IPCBlobUtils::Deserialize(aResult.get_IPCBlob());
421                 if (blobImpl && self->StoreBlobImplStream(blobImpl.forget(),
422                                                           lock) == NS_OK) {
423                   self->mState = State::READY;
424                   // By design, execution can only reach here when a caller has
425                   // called AsyncWait or AsyncLengthWait on this stream. The
426                   // underlying stream is valid, but the caller should not be
427                   // informed until that stream has data to read or it is
428                   // closed.
429                   self->WaitOnUnderlyingStream(lock);
430                   return;
431                 }
432               } else {
433                 MOZ_ASSERT(self->mState == State::CLOSED);
434                 // Callback can be called immediately
435                 self->NotifyWaitTargets(lock);
436                 return;
437               }
438             }
439             NS_WARNING("Blob data was not retrieved!");
440             self->mState = State::ERROR;
441             self->mError = aResult.type() == BlobURLDataRequestResult::Tnsresult
442                                ? aResult.get_nsresult()
443                                : NS_ERROR_FAILURE;
444             NS_ReleaseOnMainThread("BlobURLInputStream::mChannel",
445                                    self->mChannel.forget());
446             self->NotifyWaitTargets(lock);
447           },
448           [self](mozilla::ipc::ResponseRejectReason aReason) {
449             MutexAutoLock lock(self->mStateMachineMutex);
450             NS_WARNING("IPC call to SendBlobURLDataRequest failed!");
451             self->mState = State::ERROR;
452             self->mError = NS_ERROR_FAILURE;
453             NS_ReleaseOnMainThread("BlobURLInputStream::mChannel",
454                                    self->mChannel.forget());
455             self->NotifyWaitTargets(lock);
456           });
457 }
458 
StoreBlobImplStream(already_AddRefed<BlobImpl> aBlobImpl,const MutexAutoLock & aProofOfLock)459 nsresult BlobURLInputStream::StoreBlobImplStream(
460     already_AddRefed<BlobImpl> aBlobImpl, const MutexAutoLock& aProofOfLock) {
461   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
462   const RefPtr<BlobImpl> blobImpl = aBlobImpl;
463   nsAutoString contentType;
464   blobImpl->GetType(contentType);
465   mChannel->SetContentType(NS_ConvertUTF16toUTF8(contentType));
466 
467   auto cleanupOnExit = MakeScopeExit([&] { mChannel = nullptr; });
468 
469   if (blobImpl->IsFile()) {
470     nsAutoString filename;
471     blobImpl->GetName(filename);
472 
473     // Don't overwrite existing name.
474     nsString ignored;
475     bool hasName =
476         NS_SUCCEEDED(mChannel->GetContentDispositionFilename(ignored));
477 
478     if (!filename.IsEmpty() && !hasName) {
479       mChannel->SetContentDispositionFilename(filename);
480     }
481   }
482 
483   mozilla::ErrorResult errorResult;
484 
485   mBlobSize = blobImpl->GetSize(errorResult);
486 
487   if (NS_WARN_IF(errorResult.Failed())) {
488     return errorResult.StealNSResult();
489   }
490 
491   mChannel->SetContentLength(mBlobSize);
492 
493   nsCOMPtr<nsIInputStream> inputStream;
494   blobImpl->CreateInputStream(getter_AddRefs(inputStream), errorResult);
495 
496   if (NS_WARN_IF(errorResult.Failed())) {
497     return errorResult.StealNSResult();
498   }
499 
500   if (NS_WARN_IF(!inputStream)) {
501     return NS_ERROR_NOT_AVAILABLE;
502   }
503 
504   nsresult rv = NS_MakeAsyncNonBlockingInputStream(
505       inputStream.forget(), getter_AddRefs(mAsyncInputStream));
506   if (NS_WARN_IF(NS_FAILED(rv))) {
507     return rv;
508   }
509 
510   if (NS_WARN_IF(!mAsyncInputStream)) {
511     return NS_ERROR_NOT_AVAILABLE;
512   }
513 
514   return NS_OK;
515 }
516 
NotifyWaitTargets(const MutexAutoLock & aProofOfLock)517 void BlobURLInputStream::NotifyWaitTargets(const MutexAutoLock& aProofOfLock) {
518   if (mAsyncWaitCallback) {
519     auto callback = mAsyncWaitTarget
520                         ? NS_NewInputStreamReadyEvent(
521                               "BlobURLInputStream::OnInputStreamReady",
522                               mAsyncWaitCallback, mAsyncWaitTarget)
523                         : mAsyncWaitCallback;
524 
525     mAsyncWaitCallback = nullptr;
526     mAsyncWaitTarget = nullptr;
527     callback->OnInputStreamReady(this);
528   }
529 
530   if (mAsyncLengthWaitCallback) {
531     const RefPtr<BlobURLInputStream> self = this;
532     nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
533         "BlobURLInputStream::OnInputStreamLengthReady", [self] {
534           self->mAsyncLengthWaitCallback->OnInputStreamLengthReady(
535               self, self->mBlobSize);
536         });
537 
538     mAsyncLengthWaitCallback = nullptr;
539 
540     if (mAsyncLengthWaitTarget) {
541       mAsyncLengthWaitTarget->Dispatch(runnable, NS_DISPATCH_NORMAL);
542       mAsyncLengthWaitTarget = nullptr;
543     } else {
544       runnable->Run();
545     }
546   }
547 }
548 
ReleaseUnderlyingStream(const MutexAutoLock & aProofOfLock)549 void BlobURLInputStream::ReleaseUnderlyingStream(
550     const MutexAutoLock& aProofOfLock) {
551   mAsyncInputStream = nullptr;
552   mBlobSize = -1;
553 }
554 
555 }  // namespace mozilla::dom
556