|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */ |
|
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 "mozilla/Attributes.h" |
|
8 |
|
9 #include "nsIIncrementalDownload.h" |
|
10 #include "nsIRequestObserver.h" |
|
11 #include "nsIProgressEventSink.h" |
|
12 #include "nsIChannelEventSink.h" |
|
13 #include "nsIAsyncVerifyRedirectCallback.h" |
|
14 #include "nsIInterfaceRequestor.h" |
|
15 #include "nsIObserverService.h" |
|
16 #include "nsIObserver.h" |
|
17 #include "nsIFile.h" |
|
18 #include "nsITimer.h" |
|
19 #include "nsNetUtil.h" |
|
20 #include "nsAutoPtr.h" |
|
21 #include "nsWeakReference.h" |
|
22 #include "prio.h" |
|
23 #include "prprf.h" |
|
24 #include <algorithm> |
|
25 |
|
26 // Default values used to initialize a nsIncrementalDownload object. |
|
27 #define DEFAULT_CHUNK_SIZE (4096 * 16) // bytes |
|
28 #define DEFAULT_INTERVAL 60 // seconds |
|
29 |
|
30 #define UPDATE_PROGRESS_INTERVAL PRTime(500 * PR_USEC_PER_MSEC) // 500ms |
|
31 |
|
32 // Number of times to retry a failed byte-range request. |
|
33 #define MAX_RETRY_COUNT 20 |
|
34 |
|
35 //----------------------------------------------------------------------------- |
|
36 |
|
37 static nsresult |
|
38 WriteToFile(nsIFile *lf, const char *data, uint32_t len, int32_t flags) |
|
39 { |
|
40 PRFileDesc *fd; |
|
41 int32_t mode = 0600; |
|
42 nsresult rv; |
|
43 #if defined(MOZ_WIDGET_GONK) |
|
44 // The sdcard on a B2G phone looks like: |
|
45 // d---rwx--- system sdcard_rw 1970-01-01 01:00:00 sdcard |
|
46 // On the emulator, xpcshell fails when using 0600 mode to open the file, |
|
47 // and 0660 works. |
|
48 nsCOMPtr<nsIFile> parent; |
|
49 rv = lf->GetParent(getter_AddRefs(parent)); |
|
50 if (NS_FAILED(rv)) { |
|
51 return rv; |
|
52 } |
|
53 uint32_t parentPerm; |
|
54 rv = parent->GetPermissions(&parentPerm); |
|
55 if (NS_FAILED(rv)) { |
|
56 return rv; |
|
57 } |
|
58 if ((parentPerm & 0700) == 0) { |
|
59 // Parent directory has no owner-write, so try to use group permissions |
|
60 // instead of owner permissions. |
|
61 mode = 0660; |
|
62 } |
|
63 #endif |
|
64 rv = lf->OpenNSPRFileDesc(flags, mode, &fd); |
|
65 if (NS_FAILED(rv)) |
|
66 return rv; |
|
67 |
|
68 if (len) |
|
69 rv = PR_Write(fd, data, len) == int32_t(len) ? NS_OK : NS_ERROR_FAILURE; |
|
70 |
|
71 PR_Close(fd); |
|
72 return rv; |
|
73 } |
|
74 |
|
75 static nsresult |
|
76 AppendToFile(nsIFile *lf, const char *data, uint32_t len) |
|
77 { |
|
78 int32_t flags = PR_WRONLY | PR_CREATE_FILE | PR_APPEND; |
|
79 return WriteToFile(lf, data, len, flags); |
|
80 } |
|
81 |
|
82 // maxSize may be -1 if unknown |
|
83 static void |
|
84 MakeRangeSpec(const int64_t &size, const int64_t &maxSize, int32_t chunkSize, |
|
85 bool fetchRemaining, nsCString &rangeSpec) |
|
86 { |
|
87 rangeSpec.AssignLiteral("bytes="); |
|
88 rangeSpec.AppendInt(int64_t(size)); |
|
89 rangeSpec.Append('-'); |
|
90 |
|
91 if (fetchRemaining) |
|
92 return; |
|
93 |
|
94 int64_t end = size + int64_t(chunkSize); |
|
95 if (maxSize != int64_t(-1) && end > maxSize) |
|
96 end = maxSize; |
|
97 end -= 1; |
|
98 |
|
99 rangeSpec.AppendInt(int64_t(end)); |
|
100 } |
|
101 |
|
102 //----------------------------------------------------------------------------- |
|
103 |
|
104 class nsIncrementalDownload MOZ_FINAL |
|
105 : public nsIIncrementalDownload |
|
106 , public nsIStreamListener |
|
107 , public nsIObserver |
|
108 , public nsIInterfaceRequestor |
|
109 , public nsIChannelEventSink |
|
110 , public nsSupportsWeakReference |
|
111 , public nsIAsyncVerifyRedirectCallback |
|
112 { |
|
113 public: |
|
114 NS_DECL_ISUPPORTS |
|
115 NS_DECL_NSIREQUEST |
|
116 NS_DECL_NSIINCREMENTALDOWNLOAD |
|
117 NS_DECL_NSIREQUESTOBSERVER |
|
118 NS_DECL_NSISTREAMLISTENER |
|
119 NS_DECL_NSIOBSERVER |
|
120 NS_DECL_NSIINTERFACEREQUESTOR |
|
121 NS_DECL_NSICHANNELEVENTSINK |
|
122 NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK |
|
123 |
|
124 nsIncrementalDownload(); |
|
125 |
|
126 private: |
|
127 ~nsIncrementalDownload() {} |
|
128 nsresult FlushChunk(); |
|
129 void UpdateProgress(); |
|
130 nsresult CallOnStartRequest(); |
|
131 void CallOnStopRequest(); |
|
132 nsresult StartTimer(int32_t interval); |
|
133 nsresult ProcessTimeout(); |
|
134 nsresult ReadCurrentSize(); |
|
135 nsresult ClearRequestHeader(nsIHttpChannel *channel); |
|
136 |
|
137 nsCOMPtr<nsIRequestObserver> mObserver; |
|
138 nsCOMPtr<nsISupports> mObserverContext; |
|
139 nsCOMPtr<nsIProgressEventSink> mProgressSink; |
|
140 nsCOMPtr<nsIURI> mURI; |
|
141 nsCOMPtr<nsIURI> mFinalURI; |
|
142 nsCOMPtr<nsIFile> mDest; |
|
143 nsCOMPtr<nsIChannel> mChannel; |
|
144 nsCOMPtr<nsITimer> mTimer; |
|
145 nsAutoArrayPtr<char> mChunk; |
|
146 int32_t mChunkLen; |
|
147 int32_t mChunkSize; |
|
148 int32_t mInterval; |
|
149 int64_t mTotalSize; |
|
150 int64_t mCurrentSize; |
|
151 uint32_t mLoadFlags; |
|
152 int32_t mNonPartialCount; |
|
153 nsresult mStatus; |
|
154 bool mIsPending; |
|
155 bool mDidOnStartRequest; |
|
156 PRTime mLastProgressUpdate; |
|
157 nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback; |
|
158 nsCOMPtr<nsIChannel> mNewRedirectChannel; |
|
159 nsCString mPartialValidator; |
|
160 bool mCacheBust; |
|
161 }; |
|
162 |
|
163 nsIncrementalDownload::nsIncrementalDownload() |
|
164 : mChunkLen(0) |
|
165 , mChunkSize(DEFAULT_CHUNK_SIZE) |
|
166 , mInterval(DEFAULT_INTERVAL) |
|
167 , mTotalSize(-1) |
|
168 , mCurrentSize(-1) |
|
169 , mLoadFlags(LOAD_NORMAL) |
|
170 , mNonPartialCount(0) |
|
171 , mStatus(NS_OK) |
|
172 , mIsPending(false) |
|
173 , mDidOnStartRequest(false) |
|
174 , mLastProgressUpdate(0) |
|
175 , mRedirectCallback(nullptr) |
|
176 , mNewRedirectChannel(nullptr) |
|
177 , mCacheBust(false) |
|
178 { |
|
179 } |
|
180 |
|
181 nsresult |
|
182 nsIncrementalDownload::FlushChunk() |
|
183 { |
|
184 NS_ASSERTION(mTotalSize != int64_t(-1), "total size should be known"); |
|
185 |
|
186 if (mChunkLen == 0) |
|
187 return NS_OK; |
|
188 |
|
189 nsresult rv = AppendToFile(mDest, mChunk, mChunkLen); |
|
190 if (NS_FAILED(rv)) |
|
191 return rv; |
|
192 |
|
193 mCurrentSize += int64_t(mChunkLen); |
|
194 mChunkLen = 0; |
|
195 |
|
196 return NS_OK; |
|
197 } |
|
198 |
|
199 void |
|
200 nsIncrementalDownload::UpdateProgress() |
|
201 { |
|
202 mLastProgressUpdate = PR_Now(); |
|
203 |
|
204 if (mProgressSink) |
|
205 mProgressSink->OnProgress(this, mObserverContext, |
|
206 uint64_t(int64_t(mCurrentSize) + mChunkLen), |
|
207 uint64_t(int64_t(mTotalSize))); |
|
208 } |
|
209 |
|
210 nsresult |
|
211 nsIncrementalDownload::CallOnStartRequest() |
|
212 { |
|
213 if (!mObserver || mDidOnStartRequest) |
|
214 return NS_OK; |
|
215 |
|
216 mDidOnStartRequest = true; |
|
217 return mObserver->OnStartRequest(this, mObserverContext); |
|
218 } |
|
219 |
|
220 void |
|
221 nsIncrementalDownload::CallOnStopRequest() |
|
222 { |
|
223 if (!mObserver) |
|
224 return; |
|
225 |
|
226 // Ensure that OnStartRequest is always called once before OnStopRequest. |
|
227 nsresult rv = CallOnStartRequest(); |
|
228 if (NS_SUCCEEDED(mStatus)) |
|
229 mStatus = rv; |
|
230 |
|
231 mIsPending = false; |
|
232 |
|
233 mObserver->OnStopRequest(this, mObserverContext, mStatus); |
|
234 mObserver = nullptr; |
|
235 mObserverContext = nullptr; |
|
236 } |
|
237 |
|
238 nsresult |
|
239 nsIncrementalDownload::StartTimer(int32_t interval) |
|
240 { |
|
241 nsresult rv; |
|
242 mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); |
|
243 if (NS_FAILED(rv)) |
|
244 return rv; |
|
245 |
|
246 return mTimer->Init(this, interval * 1000, nsITimer::TYPE_ONE_SHOT); |
|
247 } |
|
248 |
|
249 nsresult |
|
250 nsIncrementalDownload::ProcessTimeout() |
|
251 { |
|
252 NS_ASSERTION(!mChannel, "how can we have a channel?"); |
|
253 |
|
254 // Handle existing error conditions |
|
255 if (NS_FAILED(mStatus)) { |
|
256 CallOnStopRequest(); |
|
257 return NS_OK; |
|
258 } |
|
259 |
|
260 // Fetch next chunk |
|
261 |
|
262 nsCOMPtr<nsIChannel> channel; |
|
263 nsresult rv = NS_NewChannel(getter_AddRefs(channel), mFinalURI, nullptr, |
|
264 nullptr, this, mLoadFlags); |
|
265 if (NS_FAILED(rv)) |
|
266 return rv; |
|
267 |
|
268 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(channel, &rv); |
|
269 if (NS_FAILED(rv)) |
|
270 return rv; |
|
271 |
|
272 NS_ASSERTION(mCurrentSize != int64_t(-1), |
|
273 "we should know the current file size by now"); |
|
274 |
|
275 rv = ClearRequestHeader(http); |
|
276 if (NS_FAILED(rv)) |
|
277 return rv; |
|
278 |
|
279 // Don't bother making a range request if we are just going to fetch the |
|
280 // entire document. |
|
281 if (mInterval || mCurrentSize != int64_t(0)) { |
|
282 nsAutoCString range; |
|
283 MakeRangeSpec(mCurrentSize, mTotalSize, mChunkSize, mInterval == 0, range); |
|
284 |
|
285 rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Range"), range, false); |
|
286 if (NS_FAILED(rv)) |
|
287 return rv; |
|
288 |
|
289 if (!mPartialValidator.IsEmpty()) |
|
290 http->SetRequestHeader(NS_LITERAL_CSTRING("If-Range"), |
|
291 mPartialValidator, false); |
|
292 |
|
293 if (mCacheBust) { |
|
294 http->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"), |
|
295 NS_LITERAL_CSTRING("no-cache"), false); |
|
296 http->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"), |
|
297 NS_LITERAL_CSTRING("no-cache"), false); |
|
298 } |
|
299 } |
|
300 |
|
301 rv = channel->AsyncOpen(this, nullptr); |
|
302 if (NS_FAILED(rv)) |
|
303 return rv; |
|
304 |
|
305 // Wait to assign mChannel when we know we are going to succeed. This is |
|
306 // important because we don't want to introduce a reference cycle between |
|
307 // mChannel and this until we know for a fact that AsyncOpen has succeeded, |
|
308 // thus ensuring that our stream listener methods will be invoked. |
|
309 mChannel = channel; |
|
310 return NS_OK; |
|
311 } |
|
312 |
|
313 // Reads the current file size and validates it. |
|
314 nsresult |
|
315 nsIncrementalDownload::ReadCurrentSize() |
|
316 { |
|
317 int64_t size; |
|
318 nsresult rv = mDest->GetFileSize((int64_t *) &size); |
|
319 if (rv == NS_ERROR_FILE_NOT_FOUND || |
|
320 rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { |
|
321 mCurrentSize = 0; |
|
322 return NS_OK; |
|
323 } |
|
324 if (NS_FAILED(rv)) |
|
325 return rv; |
|
326 |
|
327 mCurrentSize = size; |
|
328 return NS_OK; |
|
329 } |
|
330 |
|
331 // nsISupports |
|
332 |
|
333 NS_IMPL_ISUPPORTS(nsIncrementalDownload, |
|
334 nsIIncrementalDownload, |
|
335 nsIRequest, |
|
336 nsIStreamListener, |
|
337 nsIRequestObserver, |
|
338 nsIObserver, |
|
339 nsIInterfaceRequestor, |
|
340 nsIChannelEventSink, |
|
341 nsISupportsWeakReference, |
|
342 nsIAsyncVerifyRedirectCallback) |
|
343 |
|
344 // nsIRequest |
|
345 |
|
346 NS_IMETHODIMP |
|
347 nsIncrementalDownload::GetName(nsACString &name) |
|
348 { |
|
349 NS_ENSURE_TRUE(mURI, NS_ERROR_NOT_INITIALIZED); |
|
350 |
|
351 return mURI->GetSpec(name); |
|
352 } |
|
353 |
|
354 NS_IMETHODIMP |
|
355 nsIncrementalDownload::IsPending(bool *isPending) |
|
356 { |
|
357 *isPending = mIsPending; |
|
358 return NS_OK; |
|
359 } |
|
360 |
|
361 NS_IMETHODIMP |
|
362 nsIncrementalDownload::GetStatus(nsresult *status) |
|
363 { |
|
364 *status = mStatus; |
|
365 return NS_OK; |
|
366 } |
|
367 |
|
368 NS_IMETHODIMP |
|
369 nsIncrementalDownload::Cancel(nsresult status) |
|
370 { |
|
371 NS_ENSURE_ARG(NS_FAILED(status)); |
|
372 |
|
373 // Ignore this cancelation if we're already canceled. |
|
374 if (NS_FAILED(mStatus)) |
|
375 return NS_OK; |
|
376 |
|
377 mStatus = status; |
|
378 |
|
379 // Nothing more to do if callbacks aren't pending. |
|
380 if (!mIsPending) |
|
381 return NS_OK; |
|
382 |
|
383 if (mChannel) { |
|
384 mChannel->Cancel(mStatus); |
|
385 NS_ASSERTION(!mTimer, "what is this timer object doing here?"); |
|
386 } |
|
387 else { |
|
388 // dispatch a timer callback event to drive invoking our listener's |
|
389 // OnStopRequest. |
|
390 if (mTimer) |
|
391 mTimer->Cancel(); |
|
392 StartTimer(0); |
|
393 } |
|
394 |
|
395 return NS_OK; |
|
396 } |
|
397 |
|
398 NS_IMETHODIMP |
|
399 nsIncrementalDownload::Suspend() |
|
400 { |
|
401 return NS_ERROR_NOT_IMPLEMENTED; |
|
402 } |
|
403 |
|
404 NS_IMETHODIMP |
|
405 nsIncrementalDownload::Resume() |
|
406 { |
|
407 return NS_ERROR_NOT_IMPLEMENTED; |
|
408 } |
|
409 |
|
410 NS_IMETHODIMP |
|
411 nsIncrementalDownload::GetLoadFlags(nsLoadFlags *loadFlags) |
|
412 { |
|
413 *loadFlags = mLoadFlags; |
|
414 return NS_OK; |
|
415 } |
|
416 |
|
417 NS_IMETHODIMP |
|
418 nsIncrementalDownload::SetLoadFlags(nsLoadFlags loadFlags) |
|
419 { |
|
420 mLoadFlags = loadFlags; |
|
421 return NS_OK; |
|
422 } |
|
423 |
|
424 NS_IMETHODIMP |
|
425 nsIncrementalDownload::GetLoadGroup(nsILoadGroup **loadGroup) |
|
426 { |
|
427 return NS_ERROR_NOT_IMPLEMENTED; |
|
428 } |
|
429 |
|
430 NS_IMETHODIMP |
|
431 nsIncrementalDownload::SetLoadGroup(nsILoadGroup *loadGroup) |
|
432 { |
|
433 return NS_ERROR_NOT_IMPLEMENTED; |
|
434 } |
|
435 |
|
436 // nsIIncrementalDownload |
|
437 |
|
438 NS_IMETHODIMP |
|
439 nsIncrementalDownload::Init(nsIURI *uri, nsIFile *dest, |
|
440 int32_t chunkSize, int32_t interval) |
|
441 { |
|
442 // Keep it simple: only allow initialization once |
|
443 NS_ENSURE_FALSE(mURI, NS_ERROR_ALREADY_INITIALIZED); |
|
444 |
|
445 mDest = do_QueryInterface(dest); |
|
446 NS_ENSURE_ARG(mDest); |
|
447 |
|
448 mURI = uri; |
|
449 mFinalURI = uri; |
|
450 |
|
451 if (chunkSize > 0) |
|
452 mChunkSize = chunkSize; |
|
453 if (interval >= 0) |
|
454 mInterval = interval; |
|
455 return NS_OK; |
|
456 } |
|
457 |
|
458 NS_IMETHODIMP |
|
459 nsIncrementalDownload::GetURI(nsIURI **result) |
|
460 { |
|
461 NS_IF_ADDREF(*result = mURI); |
|
462 return NS_OK; |
|
463 } |
|
464 |
|
465 NS_IMETHODIMP |
|
466 nsIncrementalDownload::GetFinalURI(nsIURI **result) |
|
467 { |
|
468 NS_IF_ADDREF(*result = mFinalURI); |
|
469 return NS_OK; |
|
470 } |
|
471 |
|
472 NS_IMETHODIMP |
|
473 nsIncrementalDownload::GetDestination(nsIFile **result) |
|
474 { |
|
475 if (!mDest) { |
|
476 *result = nullptr; |
|
477 return NS_OK; |
|
478 } |
|
479 // Return a clone of mDest so that callers may modify the resulting nsIFile |
|
480 // without corrupting our internal object. This also works around the fact |
|
481 // that some nsIFile impls may cache the result of stat'ing the filesystem. |
|
482 return mDest->Clone(result); |
|
483 } |
|
484 |
|
485 NS_IMETHODIMP |
|
486 nsIncrementalDownload::GetTotalSize(int64_t *result) |
|
487 { |
|
488 *result = mTotalSize; |
|
489 return NS_OK; |
|
490 } |
|
491 |
|
492 NS_IMETHODIMP |
|
493 nsIncrementalDownload::GetCurrentSize(int64_t *result) |
|
494 { |
|
495 *result = mCurrentSize; |
|
496 return NS_OK; |
|
497 } |
|
498 |
|
499 NS_IMETHODIMP |
|
500 nsIncrementalDownload::Start(nsIRequestObserver *observer, |
|
501 nsISupports *context) |
|
502 { |
|
503 NS_ENSURE_ARG(observer); |
|
504 NS_ENSURE_FALSE(mIsPending, NS_ERROR_IN_PROGRESS); |
|
505 |
|
506 // Observe system shutdown so we can be sure to release any reference held |
|
507 // between ourselves and the timer. We have the observer service hold a weak |
|
508 // reference to us, so that we don't have to worry about calling |
|
509 // RemoveObserver. XXX(darin): The timer code should do this for us. |
|
510 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); |
|
511 if (obs) |
|
512 obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); |
|
513 |
|
514 nsresult rv = ReadCurrentSize(); |
|
515 if (NS_FAILED(rv)) |
|
516 return rv; |
|
517 |
|
518 rv = StartTimer(0); |
|
519 if (NS_FAILED(rv)) |
|
520 return rv; |
|
521 |
|
522 mObserver = observer; |
|
523 mObserverContext = context; |
|
524 mProgressSink = do_QueryInterface(observer); // ok if null |
|
525 |
|
526 mIsPending = true; |
|
527 return NS_OK; |
|
528 } |
|
529 |
|
530 // nsIRequestObserver |
|
531 |
|
532 NS_IMETHODIMP |
|
533 nsIncrementalDownload::OnStartRequest(nsIRequest *request, |
|
534 nsISupports *context) |
|
535 { |
|
536 nsresult rv; |
|
537 |
|
538 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(request, &rv); |
|
539 if (NS_FAILED(rv)) |
|
540 return rv; |
|
541 |
|
542 // Ensure that we are receiving a 206 response. |
|
543 uint32_t code; |
|
544 rv = http->GetResponseStatus(&code); |
|
545 if (NS_FAILED(rv)) |
|
546 return rv; |
|
547 if (code != 206) { |
|
548 // We may already have the entire file downloaded, in which case |
|
549 // our request for a range beyond the end of the file would have |
|
550 // been met with an error response code. |
|
551 if (code == 416 && mTotalSize == int64_t(-1)) { |
|
552 mTotalSize = mCurrentSize; |
|
553 // Return an error code here to suppress OnDataAvailable. |
|
554 return NS_ERROR_DOWNLOAD_COMPLETE; |
|
555 } |
|
556 // The server may have decided to give us all of the data in one chunk. If |
|
557 // we requested a partial range, then we don't want to download all of the |
|
558 // data at once. So, we'll just try again, but if this keeps happening then |
|
559 // we'll eventually give up. |
|
560 if (code == 200) { |
|
561 if (mInterval) { |
|
562 mChannel = nullptr; |
|
563 if (++mNonPartialCount > MAX_RETRY_COUNT) { |
|
564 NS_WARNING("unable to fetch a byte range; giving up"); |
|
565 return NS_ERROR_FAILURE; |
|
566 } |
|
567 // Increase delay with each failure. |
|
568 StartTimer(mInterval * mNonPartialCount); |
|
569 return NS_ERROR_DOWNLOAD_NOT_PARTIAL; |
|
570 } |
|
571 // Since we have been asked to download the rest of the file, we can deal |
|
572 // with a 200 response. This may result in downloading the beginning of |
|
573 // the file again, but that can't really be helped. |
|
574 } else { |
|
575 NS_WARNING("server response was unexpected"); |
|
576 return NS_ERROR_UNEXPECTED; |
|
577 } |
|
578 } else { |
|
579 // We got a partial response, so clear this counter in case the next chunk |
|
580 // results in a 200 response. |
|
581 mNonPartialCount = 0; |
|
582 |
|
583 // confirm that the content-range response header is consistent with |
|
584 // expectations on each 206. If it is not then drop this response and |
|
585 // retry with no-cache set. |
|
586 if (!mCacheBust) { |
|
587 nsAutoCString buf; |
|
588 int64_t startByte = 0; |
|
589 bool confirmedOK = false; |
|
590 |
|
591 rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf); |
|
592 if (NS_FAILED(rv)) |
|
593 return rv; // it isn't a useful 206 without a CONTENT-RANGE of some sort |
|
594 |
|
595 // Content-Range: bytes 0-299999/25604694 |
|
596 int32_t p = buf.Find("bytes "); |
|
597 |
|
598 // first look for the starting point of the content-range |
|
599 // to make sure it is what we expect |
|
600 if (p != -1) { |
|
601 char *endptr = nullptr; |
|
602 const char *s = buf.get() + p + 6; |
|
603 while (*s && *s == ' ') |
|
604 s++; |
|
605 startByte = strtol(s, &endptr, 10); |
|
606 |
|
607 if (*s && endptr && (endptr != s) && |
|
608 (mCurrentSize == startByte)) { |
|
609 |
|
610 // ok the starting point is confirmed. We still need to check the |
|
611 // total size of the range for consistency if this isn't |
|
612 // the first chunk |
|
613 if (mTotalSize == int64_t(-1)) { |
|
614 // first chunk |
|
615 confirmedOK = true; |
|
616 } else { |
|
617 int32_t slash = buf.FindChar('/'); |
|
618 int64_t rangeSize = 0; |
|
619 if (slash != kNotFound && |
|
620 (PR_sscanf(buf.get() + slash + 1, "%lld", (int64_t *) &rangeSize) == 1) && |
|
621 rangeSize == mTotalSize) { |
|
622 confirmedOK = true; |
|
623 } |
|
624 } |
|
625 } |
|
626 } |
|
627 |
|
628 if (!confirmedOK) { |
|
629 NS_WARNING("unexpected content-range"); |
|
630 mCacheBust = true; |
|
631 mChannel = nullptr; |
|
632 if (++mNonPartialCount > MAX_RETRY_COUNT) { |
|
633 NS_WARNING("unable to fetch a byte range; giving up"); |
|
634 return NS_ERROR_FAILURE; |
|
635 } |
|
636 // Increase delay with each failure. |
|
637 StartTimer(mInterval * mNonPartialCount); |
|
638 return NS_ERROR_DOWNLOAD_NOT_PARTIAL; |
|
639 } |
|
640 } |
|
641 } |
|
642 |
|
643 // Do special processing after the first response. |
|
644 if (mTotalSize == int64_t(-1)) { |
|
645 // Update knowledge of mFinalURI |
|
646 rv = http->GetURI(getter_AddRefs(mFinalURI)); |
|
647 if (NS_FAILED(rv)) |
|
648 return rv; |
|
649 http->GetResponseHeader(NS_LITERAL_CSTRING("Etag"), mPartialValidator); |
|
650 if (StringBeginsWith(mPartialValidator, NS_LITERAL_CSTRING("W/"))) |
|
651 mPartialValidator.Truncate(); // don't use weak validators |
|
652 if (mPartialValidator.IsEmpty()) |
|
653 http->GetResponseHeader(NS_LITERAL_CSTRING("Last-Modified"), mPartialValidator); |
|
654 |
|
655 if (code == 206) { |
|
656 // OK, read the Content-Range header to determine the total size of this |
|
657 // download file. |
|
658 nsAutoCString buf; |
|
659 rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf); |
|
660 if (NS_FAILED(rv)) |
|
661 return rv; |
|
662 int32_t slash = buf.FindChar('/'); |
|
663 if (slash == kNotFound) { |
|
664 NS_WARNING("server returned invalid Content-Range header!"); |
|
665 return NS_ERROR_UNEXPECTED; |
|
666 } |
|
667 if (PR_sscanf(buf.get() + slash + 1, "%lld", (int64_t *) &mTotalSize) != 1) |
|
668 return NS_ERROR_UNEXPECTED; |
|
669 } else { |
|
670 rv = http->GetContentLength(&mTotalSize); |
|
671 if (NS_FAILED(rv)) |
|
672 return rv; |
|
673 // We need to know the total size of the thing we're trying to download. |
|
674 if (mTotalSize == int64_t(-1)) { |
|
675 NS_WARNING("server returned no content-length header!"); |
|
676 return NS_ERROR_UNEXPECTED; |
|
677 } |
|
678 // Need to truncate (or create, if it doesn't exist) the file since we |
|
679 // are downloading the whole thing. |
|
680 WriteToFile(mDest, nullptr, 0, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE); |
|
681 mCurrentSize = 0; |
|
682 } |
|
683 |
|
684 // Notify observer that we are starting... |
|
685 rv = CallOnStartRequest(); |
|
686 if (NS_FAILED(rv)) |
|
687 return rv; |
|
688 } |
|
689 |
|
690 // Adjust mChunkSize accordingly if mCurrentSize is close to mTotalSize. |
|
691 int64_t diff = mTotalSize - mCurrentSize; |
|
692 if (diff <= int64_t(0)) { |
|
693 NS_WARNING("about to set a bogus chunk size; giving up"); |
|
694 return NS_ERROR_UNEXPECTED; |
|
695 } |
|
696 |
|
697 if (diff < int64_t(mChunkSize)) |
|
698 mChunkSize = uint32_t(diff); |
|
699 |
|
700 mChunk = new char[mChunkSize]; |
|
701 if (!mChunk) |
|
702 rv = NS_ERROR_OUT_OF_MEMORY; |
|
703 |
|
704 return rv; |
|
705 } |
|
706 |
|
707 NS_IMETHODIMP |
|
708 nsIncrementalDownload::OnStopRequest(nsIRequest *request, |
|
709 nsISupports *context, |
|
710 nsresult status) |
|
711 { |
|
712 // Not a real error; just a trick to kill off the channel without our |
|
713 // listener having to care. |
|
714 if (status == NS_ERROR_DOWNLOAD_NOT_PARTIAL) |
|
715 return NS_OK; |
|
716 |
|
717 // Not a real error; just a trick used to suppress OnDataAvailable calls. |
|
718 if (status == NS_ERROR_DOWNLOAD_COMPLETE) |
|
719 status = NS_OK; |
|
720 |
|
721 if (NS_SUCCEEDED(mStatus)) |
|
722 mStatus = status; |
|
723 |
|
724 if (mChunk) { |
|
725 if (NS_SUCCEEDED(mStatus)) |
|
726 mStatus = FlushChunk(); |
|
727 |
|
728 mChunk = nullptr; // deletes memory |
|
729 mChunkLen = 0; |
|
730 UpdateProgress(); |
|
731 } |
|
732 |
|
733 mChannel = nullptr; |
|
734 |
|
735 // Notify listener if we hit an error or finished |
|
736 if (NS_FAILED(mStatus) || mCurrentSize == mTotalSize) { |
|
737 CallOnStopRequest(); |
|
738 return NS_OK; |
|
739 } |
|
740 |
|
741 return StartTimer(mInterval); // Do next chunk |
|
742 } |
|
743 |
|
744 // nsIStreamListener |
|
745 |
|
746 NS_IMETHODIMP |
|
747 nsIncrementalDownload::OnDataAvailable(nsIRequest *request, |
|
748 nsISupports *context, |
|
749 nsIInputStream *input, |
|
750 uint64_t offset, |
|
751 uint32_t count) |
|
752 { |
|
753 while (count) { |
|
754 uint32_t space = mChunkSize - mChunkLen; |
|
755 uint32_t n, len = std::min(space, count); |
|
756 |
|
757 nsresult rv = input->Read(mChunk + mChunkLen, len, &n); |
|
758 if (NS_FAILED(rv)) |
|
759 return rv; |
|
760 if (n != len) |
|
761 return NS_ERROR_UNEXPECTED; |
|
762 |
|
763 count -= n; |
|
764 mChunkLen += n; |
|
765 |
|
766 if (mChunkLen == mChunkSize) { |
|
767 rv = FlushChunk(); |
|
768 if (NS_FAILED(rv)) |
|
769 return rv; |
|
770 } |
|
771 } |
|
772 |
|
773 if (PR_Now() > mLastProgressUpdate + UPDATE_PROGRESS_INTERVAL) |
|
774 UpdateProgress(); |
|
775 |
|
776 return NS_OK; |
|
777 } |
|
778 |
|
779 // nsIObserver |
|
780 |
|
781 NS_IMETHODIMP |
|
782 nsIncrementalDownload::Observe(nsISupports *subject, const char *topic, |
|
783 const char16_t *data) |
|
784 { |
|
785 if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { |
|
786 Cancel(NS_ERROR_ABORT); |
|
787 |
|
788 // Since the app is shutting down, we need to go ahead and notify our |
|
789 // observer here. Otherwise, we would notify them after XPCOM has been |
|
790 // shutdown or not at all. |
|
791 CallOnStopRequest(); |
|
792 } |
|
793 else if (strcmp(topic, NS_TIMER_CALLBACK_TOPIC) == 0) { |
|
794 mTimer = nullptr; |
|
795 nsresult rv = ProcessTimeout(); |
|
796 if (NS_FAILED(rv)) |
|
797 Cancel(rv); |
|
798 } |
|
799 return NS_OK; |
|
800 } |
|
801 |
|
802 // nsIInterfaceRequestor |
|
803 |
|
804 NS_IMETHODIMP |
|
805 nsIncrementalDownload::GetInterface(const nsIID &iid, void **result) |
|
806 { |
|
807 if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) { |
|
808 NS_ADDREF_THIS(); |
|
809 *result = static_cast<nsIChannelEventSink *>(this); |
|
810 return NS_OK; |
|
811 } |
|
812 |
|
813 nsCOMPtr<nsIInterfaceRequestor> ir = do_QueryInterface(mObserver); |
|
814 if (ir) |
|
815 return ir->GetInterface(iid, result); |
|
816 |
|
817 return NS_ERROR_NO_INTERFACE; |
|
818 } |
|
819 |
|
820 nsresult |
|
821 nsIncrementalDownload::ClearRequestHeader(nsIHttpChannel *channel) |
|
822 { |
|
823 NS_ENSURE_ARG(channel); |
|
824 |
|
825 // We don't support encodings -- they make the Content-Length not equal |
|
826 // to the actual size of the data. |
|
827 return channel->SetRequestHeader(NS_LITERAL_CSTRING("Accept-Encoding"), |
|
828 NS_LITERAL_CSTRING(""), false); |
|
829 } |
|
830 |
|
831 // nsIChannelEventSink |
|
832 |
|
833 NS_IMETHODIMP |
|
834 nsIncrementalDownload::AsyncOnChannelRedirect(nsIChannel *oldChannel, |
|
835 nsIChannel *newChannel, |
|
836 uint32_t flags, |
|
837 nsIAsyncVerifyRedirectCallback *cb) |
|
838 { |
|
839 // In response to a redirect, we need to propagate the Range header. See bug |
|
840 // 311595. Any failure code returned from this function aborts the redirect. |
|
841 |
|
842 nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(oldChannel); |
|
843 NS_ENSURE_STATE(http); |
|
844 |
|
845 nsCOMPtr<nsIHttpChannel> newHttpChannel = do_QueryInterface(newChannel); |
|
846 NS_ENSURE_STATE(newHttpChannel); |
|
847 |
|
848 NS_NAMED_LITERAL_CSTRING(rangeHdr, "Range"); |
|
849 |
|
850 nsresult rv = ClearRequestHeader(newHttpChannel); |
|
851 if (NS_FAILED(rv)) |
|
852 return rv; |
|
853 |
|
854 // If we didn't have a Range header, then we must be doing a full download. |
|
855 nsAutoCString rangeVal; |
|
856 http->GetRequestHeader(rangeHdr, rangeVal); |
|
857 if (!rangeVal.IsEmpty()) { |
|
858 rv = newHttpChannel->SetRequestHeader(rangeHdr, rangeVal, false); |
|
859 NS_ENSURE_SUCCESS(rv, rv); |
|
860 } |
|
861 |
|
862 // A redirection changes the validator |
|
863 mPartialValidator.Truncate(); |
|
864 |
|
865 if (mCacheBust) { |
|
866 newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"), |
|
867 NS_LITERAL_CSTRING("no-cache"), false); |
|
868 newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"), |
|
869 NS_LITERAL_CSTRING("no-cache"), false); |
|
870 } |
|
871 |
|
872 // Prepare to receive callback |
|
873 mRedirectCallback = cb; |
|
874 mNewRedirectChannel = newChannel; |
|
875 |
|
876 // Give the observer a chance to see this redirect notification. |
|
877 nsCOMPtr<nsIChannelEventSink> sink = do_GetInterface(mObserver); |
|
878 if (sink) { |
|
879 rv = sink->AsyncOnChannelRedirect(oldChannel, newChannel, flags, this); |
|
880 if (NS_FAILED(rv)) { |
|
881 mRedirectCallback = nullptr; |
|
882 mNewRedirectChannel = nullptr; |
|
883 } |
|
884 return rv; |
|
885 } |
|
886 (void) OnRedirectVerifyCallback(NS_OK); |
|
887 return NS_OK; |
|
888 } |
|
889 |
|
890 NS_IMETHODIMP |
|
891 nsIncrementalDownload::OnRedirectVerifyCallback(nsresult result) |
|
892 { |
|
893 NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback"); |
|
894 NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback"); |
|
895 |
|
896 // Update mChannel, so we can Cancel the new channel. |
|
897 if (NS_SUCCEEDED(result)) |
|
898 mChannel = mNewRedirectChannel; |
|
899 |
|
900 mRedirectCallback->OnRedirectVerifyCallback(result); |
|
901 mRedirectCallback = nullptr; |
|
902 mNewRedirectChannel = nullptr; |
|
903 return NS_OK; |
|
904 } |
|
905 |
|
906 extern nsresult |
|
907 net_NewIncrementalDownload(nsISupports *outer, const nsIID &iid, void **result) |
|
908 { |
|
909 if (outer) |
|
910 return NS_ERROR_NO_AGGREGATION; |
|
911 |
|
912 nsIncrementalDownload *d = new nsIncrementalDownload(); |
|
913 if (!d) |
|
914 return NS_ERROR_OUT_OF_MEMORY; |
|
915 |
|
916 NS_ADDREF(d); |
|
917 nsresult rv = d->QueryInterface(iid, result); |
|
918 NS_RELEASE(d); |
|
919 return rv; |
|
920 } |