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 "FetchStreamReader.h"
8 #include "InternalResponse.h"
9 #include "mozilla/dom/PromiseBinding.h"
10 #include "mozilla/SystemGroup.h"
11 #include "mozilla/TaskCategory.h"
12 #include "nsContentUtils.h"
13 #include "nsIScriptError.h"
14 #include "nsPIDOMWindow.h"
15 
16 namespace mozilla {
17 namespace dom {
18 
19 namespace {
20 
21 class FetchStreamReaderWorkerHolder final : public WorkerHolder {
22  public:
FetchStreamReaderWorkerHolder(FetchStreamReader * aReader)23   explicit FetchStreamReaderWorkerHolder(FetchStreamReader* aReader)
24       : WorkerHolder("FetchStreamReaderWorkerHolder",
25                      WorkerHolder::Behavior::AllowIdleShutdownStart),
26         mReader(aReader),
27         mWasNotified(false) {}
28 
Notify(WorkerStatus aStatus)29   bool Notify(WorkerStatus aStatus) override {
30     if (!mWasNotified) {
31       mWasNotified = true;
32       // The WorkerPrivate does have a context available, and we could pass it
33       // here to trigger cancellation of the reader, but the author of this
34       // comment chickened out.
35       mReader->CloseAndRelease(nullptr, NS_ERROR_DOM_INVALID_STATE_ERR);
36     }
37 
38     return true;
39   }
40 
41  private:
42   RefPtr<FetchStreamReader> mReader;
43   bool mWasNotified;
44 };
45 
46 }  // namespace
47 
48 NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader)
NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader)49 NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader)
50 
51 NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader)
52 
53 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader)
54   NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
55 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
56 
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader)
58   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
60 
61 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader)
62   NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader)
63 NS_IMPL_CYCLE_COLLECTION_TRACE_END
64 
65 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader)
66   NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback)
67   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStreamCallback)
68 NS_INTERFACE_MAP_END
69 
70 /* static */ nsresult FetchStreamReader::Create(
71     JSContext* aCx, nsIGlobalObject* aGlobal, FetchStreamReader** aStreamReader,
72     nsIInputStream** aInputStream) {
73   MOZ_ASSERT(aCx);
74   MOZ_ASSERT(aGlobal);
75   MOZ_ASSERT(aStreamReader);
76   MOZ_ASSERT(aInputStream);
77 
78   RefPtr<FetchStreamReader> streamReader = new FetchStreamReader(aGlobal);
79 
80   nsCOMPtr<nsIAsyncInputStream> pipeIn;
81 
82   nsresult rv =
83       NS_NewPipe2(getter_AddRefs(pipeIn),
84                   getter_AddRefs(streamReader->mPipeOut), true, true, 0, 0);
85   if (NS_WARN_IF(NS_FAILED(rv))) {
86     return rv;
87   }
88 
89   if (!NS_IsMainThread()) {
90     WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
91     MOZ_ASSERT(workerPrivate);
92 
93     // We need to know when the worker goes away.
94     UniquePtr<FetchStreamReaderWorkerHolder> holder(
95         new FetchStreamReaderWorkerHolder(streamReader));
96     if (NS_WARN_IF(!holder->HoldWorker(workerPrivate, Closing))) {
97       streamReader->mPipeOut->CloseWithStatus(NS_ERROR_DOM_INVALID_STATE_ERR);
98       return NS_ERROR_DOM_INVALID_STATE_ERR;
99     }
100 
101     // These 2 objects create a ref-cycle here that is broken when the stream is
102     // closed or the worker shutsdown.
103     streamReader->mWorkerHolder = Move(holder);
104   }
105 
106   pipeIn.forget(aInputStream);
107   streamReader.forget(aStreamReader);
108   return NS_OK;
109 }
110 
FetchStreamReader(nsIGlobalObject * aGlobal)111 FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal)
112     : mGlobal(aGlobal),
113       mOwningEventTarget(mGlobal->EventTargetFor(TaskCategory::Other)),
114       mBufferRemaining(0),
115       mBufferOffset(0),
116       mStreamClosed(false) {
117   MOZ_ASSERT(aGlobal);
118 }
119 
~FetchStreamReader()120 FetchStreamReader::~FetchStreamReader() {
121   CloseAndRelease(nullptr, NS_BASE_STREAM_CLOSED);
122 }
123 
124 // If a context is provided, an attempt will be made to cancel the reader.  The
125 // only situation where we don't expect to have a context is when closure is
126 // being triggered from the destructor or the WorkerHolder is notifying.  If
127 // we're at the destructor, it's far too late to cancel anything.  And if the
128 // WorkerHolder is being notified, the global is going away, so there's also
129 // no need to do further JS work.
CloseAndRelease(JSContext * aCx,nsresult aStatus)130 void FetchStreamReader::CloseAndRelease(JSContext* aCx, nsresult aStatus) {
131   NS_ASSERT_OWNINGTHREAD(FetchStreamReader);
132 
133   if (mStreamClosed) {
134     // Already closed.
135     return;
136   }
137 
138   RefPtr<FetchStreamReader> kungFuDeathGrip = this;
139 
140   if (aCx) {
141     MOZ_ASSERT(mReader);
142 
143     RefPtr<DOMException> error = DOMException::Create(aStatus);
144 
145     JS::Rooted<JS::Value> errorValue(aCx);
146     if (ToJSValue(aCx, error, &errorValue)) {
147       JS::Rooted<JSObject*> reader(aCx, mReader);
148       // It's currently safe to cancel an already closed reader because, per the
149       // comments in ReadableStream::cancel() conveying the spec, step 2 of
150       // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is
151       // "closed", return a new promise resolved with undefined.
152       JS::ReadableStreamReaderCancel(aCx, reader, errorValue);
153     }
154   }
155 
156   mStreamClosed = true;
157 
158   mGlobal = nullptr;
159 
160   mPipeOut->CloseWithStatus(aStatus);
161   mPipeOut = nullptr;
162 
163   mWorkerHolder = nullptr;
164 
165   mReader = nullptr;
166   mBuffer = nullptr;
167 }
168 
StartConsuming(JSContext * aCx,JS::HandleObject aStream,JS::MutableHandle<JSObject * > aReader,ErrorResult & aRv)169 void FetchStreamReader::StartConsuming(JSContext* aCx, JS::HandleObject aStream,
170                                        JS::MutableHandle<JSObject*> aReader,
171                                        ErrorResult& aRv) {
172   MOZ_DIAGNOSTIC_ASSERT(!mReader);
173   MOZ_DIAGNOSTIC_ASSERT(aStream);
174 
175   JS::Rooted<JSObject*> reader(
176       aCx, JS::ReadableStreamGetReader(aCx, aStream,
177                                        JS::ReadableStreamReaderMode::Default));
178   if (!reader) {
179     aRv.StealExceptionFromJSContext(aCx);
180     CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
181     return;
182   }
183 
184   mReader = reader;
185   aReader.set(reader);
186 
187   aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
188   if (NS_WARN_IF(aRv.Failed())) {
189     return;
190   }
191 }
192 
193 // nsIOutputStreamCallback interface
194 
195 NS_IMETHODIMP
OnOutputStreamReady(nsIAsyncOutputStream * aStream)196 FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) {
197   NS_ASSERT_OWNINGTHREAD(FetchStreamReader);
198   MOZ_ASSERT(aStream == mPipeOut);
199   MOZ_ASSERT(mReader);
200 
201   if (mStreamClosed) {
202     return NS_OK;
203   }
204 
205   if (mBuffer) {
206     return WriteBuffer();
207   }
208 
209   // TODO: We need to verify this is the correct global per the spec.
210   //       See bug 1385890.
211   AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerHolder);
212 
213   JS::Rooted<JSObject*> reader(aes.cx(), mReader);
214   JS::Rooted<JSObject*> promise(
215       aes.cx(), JS::ReadableStreamDefaultReaderRead(aes.cx(), reader));
216   if (NS_WARN_IF(!promise)) {
217     // Let's close the stream.
218     CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
219     return NS_ERROR_FAILURE;
220   }
221 
222   RefPtr<Promise> domPromise = Promise::CreateFromExisting(mGlobal, promise);
223   if (NS_WARN_IF(!domPromise)) {
224     // Let's close the stream.
225     CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
226     return NS_ERROR_FAILURE;
227   }
228 
229   // Let's wait.
230   domPromise->AppendNativeHandler(this);
231   return NS_OK;
232 }
233 
ResolvedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)234 void FetchStreamReader::ResolvedCallback(JSContext* aCx,
235                                          JS::Handle<JS::Value> aValue) {
236   if (mStreamClosed) {
237     return;
238   }
239 
240   // This promise should be resolved with { done: boolean, value: something },
241   // "value" is interesting only if done is false.
242 
243   // We don't want to play with JS api, let's WebIDL bindings doing it for us.
244   // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the
245   // parsing succeeded, we can proceed with the parsing of the "value", which it
246   // must be a Uint8Array.
247   FetchReadableStreamReadDataDone valueDone;
248   if (!valueDone.Init(aCx, aValue)) {
249     JS_ClearPendingException(aCx);
250     CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
251     return;
252   }
253 
254   if (valueDone.mDone) {
255     // Stream is completed.
256     CloseAndRelease(aCx, NS_BASE_STREAM_CLOSED);
257     return;
258   }
259 
260   UniquePtr<FetchReadableStreamReadDataArray> value(
261       new FetchReadableStreamReadDataArray);
262   if (!value->Init(aCx, aValue) || !value->mValue.WasPassed()) {
263     JS_ClearPendingException(aCx);
264     CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
265     return;
266   }
267 
268   Uint8Array& array = value->mValue.Value();
269   array.ComputeLengthAndData();
270   uint32_t len = array.Length();
271 
272   if (len == 0) {
273     // If there is nothing to read, let's do another reading.
274     OnOutputStreamReady(mPipeOut);
275     return;
276   }
277 
278   MOZ_DIAGNOSTIC_ASSERT(!mBuffer);
279   mBuffer = Move(value);
280 
281   mBufferOffset = 0;
282   mBufferRemaining = len;
283 
284   nsresult rv = WriteBuffer();
285   if (NS_FAILED(rv)) {
286     // DOMException only understands errors from domerr.msg, so we normalize to
287     // identifying an abort if the write fails.
288     CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR);
289   }
290 }
291 
WriteBuffer()292 nsresult FetchStreamReader::WriteBuffer() {
293   MOZ_ASSERT(mBuffer);
294   MOZ_ASSERT(mBuffer->mValue.WasPassed());
295 
296   Uint8Array& array = mBuffer->mValue.Value();
297   char* data = reinterpret_cast<char*>(array.Data());
298 
299   while (1) {
300     uint32_t written = 0;
301     nsresult rv =
302         mPipeOut->Write(data + mBufferOffset, mBufferRemaining, &written);
303 
304     if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
305       break;
306     }
307 
308     if (NS_WARN_IF(NS_FAILED(rv))) {
309       return rv;
310     }
311 
312     MOZ_ASSERT(written <= mBufferRemaining);
313     mBufferRemaining -= written;
314     mBufferOffset += written;
315 
316     if (mBufferRemaining == 0) {
317       mBuffer = nullptr;
318       break;
319     }
320   }
321 
322   nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
323   if (NS_WARN_IF(NS_FAILED(rv))) {
324     return rv;
325   }
326 
327   return NS_OK;
328 }
329 
RejectedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)330 void FetchStreamReader::RejectedCallback(JSContext* aCx,
331                                          JS::Handle<JS::Value> aValue) {
332   ReportErrorToConsole(aCx, aValue);
333   CloseAndRelease(aCx, NS_ERROR_FAILURE);
334 }
335 
ReportErrorToConsole(JSContext * aCx,JS::Handle<JS::Value> aValue)336 void FetchStreamReader::ReportErrorToConsole(JSContext* aCx,
337                                              JS::Handle<JS::Value> aValue) {
338   nsCString sourceSpec;
339   uint32_t line = 0;
340   uint32_t column = 0;
341   nsString valueString;
342 
343   nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
344                                      valueString);
345 
346   nsTArray<nsString> params;
347   params.AppendElement(valueString);
348 
349   RefPtr<ConsoleReportCollector> reporter = new ConsoleReportCollector();
350   reporter->AddConsoleReport(
351       nsIScriptError::errorFlag,
352       NS_LITERAL_CSTRING("ReadableStreamReader.read"),
353       nsContentUtils::eDOM_PROPERTIES, sourceSpec, line, column,
354       NS_LITERAL_CSTRING("ReadableStreamReadingFailed"), params);
355 
356   uint64_t innerWindowId = 0;
357 
358   if (NS_IsMainThread()) {
359     nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
360     if (window) {
361       innerWindowId = window->WindowID();
362     }
363     reporter->FlushReportsToConsole(innerWindowId);
364     return;
365   }
366 
367   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
368   if (workerPrivate) {
369     innerWindowId = workerPrivate->WindowID();
370   }
371 
372   RefPtr<Runnable> r = NS_NewRunnableFunction(
373       "FetchStreamReader::ReportErrorToConsole", [reporter, innerWindowId]() {
374         reporter->FlushReportsToConsole(innerWindowId);
375       });
376 
377   workerPrivate->DispatchToMainThread(r.forget());
378 }
379 
380 }  // namespace dom
381 }  // namespace mozilla
382