michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/Attributes.h" michael@0: michael@0: #include "nsIIncrementalDownload.h" michael@0: #include "nsIRequestObserver.h" michael@0: #include "nsIProgressEventSink.h" michael@0: #include "nsIChannelEventSink.h" michael@0: #include "nsIAsyncVerifyRedirectCallback.h" michael@0: #include "nsIInterfaceRequestor.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsIObserver.h" michael@0: #include "nsIFile.h" michael@0: #include "nsITimer.h" michael@0: #include "nsNetUtil.h" michael@0: #include "nsAutoPtr.h" michael@0: #include "nsWeakReference.h" michael@0: #include "prio.h" michael@0: #include "prprf.h" michael@0: #include michael@0: michael@0: // Default values used to initialize a nsIncrementalDownload object. michael@0: #define DEFAULT_CHUNK_SIZE (4096 * 16) // bytes michael@0: #define DEFAULT_INTERVAL 60 // seconds michael@0: michael@0: #define UPDATE_PROGRESS_INTERVAL PRTime(500 * PR_USEC_PER_MSEC) // 500ms michael@0: michael@0: // Number of times to retry a failed byte-range request. michael@0: #define MAX_RETRY_COUNT 20 michael@0: michael@0: //----------------------------------------------------------------------------- michael@0: michael@0: static nsresult michael@0: WriteToFile(nsIFile *lf, const char *data, uint32_t len, int32_t flags) michael@0: { michael@0: PRFileDesc *fd; michael@0: int32_t mode = 0600; michael@0: nsresult rv; michael@0: #if defined(MOZ_WIDGET_GONK) michael@0: // The sdcard on a B2G phone looks like: michael@0: // d---rwx--- system sdcard_rw 1970-01-01 01:00:00 sdcard michael@0: // On the emulator, xpcshell fails when using 0600 mode to open the file, michael@0: // and 0660 works. michael@0: nsCOMPtr parent; michael@0: rv = lf->GetParent(getter_AddRefs(parent)); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: uint32_t parentPerm; michael@0: rv = parent->GetPermissions(&parentPerm); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: if ((parentPerm & 0700) == 0) { michael@0: // Parent directory has no owner-write, so try to use group permissions michael@0: // instead of owner permissions. michael@0: mode = 0660; michael@0: } michael@0: #endif michael@0: rv = lf->OpenNSPRFileDesc(flags, mode, &fd); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: if (len) michael@0: rv = PR_Write(fd, data, len) == int32_t(len) ? NS_OK : NS_ERROR_FAILURE; michael@0: michael@0: PR_Close(fd); michael@0: return rv; michael@0: } michael@0: michael@0: static nsresult michael@0: AppendToFile(nsIFile *lf, const char *data, uint32_t len) michael@0: { michael@0: int32_t flags = PR_WRONLY | PR_CREATE_FILE | PR_APPEND; michael@0: return WriteToFile(lf, data, len, flags); michael@0: } michael@0: michael@0: // maxSize may be -1 if unknown michael@0: static void michael@0: MakeRangeSpec(const int64_t &size, const int64_t &maxSize, int32_t chunkSize, michael@0: bool fetchRemaining, nsCString &rangeSpec) michael@0: { michael@0: rangeSpec.AssignLiteral("bytes="); michael@0: rangeSpec.AppendInt(int64_t(size)); michael@0: rangeSpec.Append('-'); michael@0: michael@0: if (fetchRemaining) michael@0: return; michael@0: michael@0: int64_t end = size + int64_t(chunkSize); michael@0: if (maxSize != int64_t(-1) && end > maxSize) michael@0: end = maxSize; michael@0: end -= 1; michael@0: michael@0: rangeSpec.AppendInt(int64_t(end)); michael@0: } michael@0: michael@0: //----------------------------------------------------------------------------- michael@0: michael@0: class nsIncrementalDownload MOZ_FINAL michael@0: : public nsIIncrementalDownload michael@0: , public nsIStreamListener michael@0: , public nsIObserver michael@0: , public nsIInterfaceRequestor michael@0: , public nsIChannelEventSink michael@0: , public nsSupportsWeakReference michael@0: , public nsIAsyncVerifyRedirectCallback michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSIREQUEST michael@0: NS_DECL_NSIINCREMENTALDOWNLOAD michael@0: NS_DECL_NSIREQUESTOBSERVER michael@0: NS_DECL_NSISTREAMLISTENER michael@0: NS_DECL_NSIOBSERVER michael@0: NS_DECL_NSIINTERFACEREQUESTOR michael@0: NS_DECL_NSICHANNELEVENTSINK michael@0: NS_DECL_NSIASYNCVERIFYREDIRECTCALLBACK michael@0: michael@0: nsIncrementalDownload(); michael@0: michael@0: private: michael@0: ~nsIncrementalDownload() {} michael@0: nsresult FlushChunk(); michael@0: void UpdateProgress(); michael@0: nsresult CallOnStartRequest(); michael@0: void CallOnStopRequest(); michael@0: nsresult StartTimer(int32_t interval); michael@0: nsresult ProcessTimeout(); michael@0: nsresult ReadCurrentSize(); michael@0: nsresult ClearRequestHeader(nsIHttpChannel *channel); michael@0: michael@0: nsCOMPtr mObserver; michael@0: nsCOMPtr mObserverContext; michael@0: nsCOMPtr mProgressSink; michael@0: nsCOMPtr mURI; michael@0: nsCOMPtr mFinalURI; michael@0: nsCOMPtr mDest; michael@0: nsCOMPtr mChannel; michael@0: nsCOMPtr mTimer; michael@0: nsAutoArrayPtr mChunk; michael@0: int32_t mChunkLen; michael@0: int32_t mChunkSize; michael@0: int32_t mInterval; michael@0: int64_t mTotalSize; michael@0: int64_t mCurrentSize; michael@0: uint32_t mLoadFlags; michael@0: int32_t mNonPartialCount; michael@0: nsresult mStatus; michael@0: bool mIsPending; michael@0: bool mDidOnStartRequest; michael@0: PRTime mLastProgressUpdate; michael@0: nsCOMPtr mRedirectCallback; michael@0: nsCOMPtr mNewRedirectChannel; michael@0: nsCString mPartialValidator; michael@0: bool mCacheBust; michael@0: }; michael@0: michael@0: nsIncrementalDownload::nsIncrementalDownload() michael@0: : mChunkLen(0) michael@0: , mChunkSize(DEFAULT_CHUNK_SIZE) michael@0: , mInterval(DEFAULT_INTERVAL) michael@0: , mTotalSize(-1) michael@0: , mCurrentSize(-1) michael@0: , mLoadFlags(LOAD_NORMAL) michael@0: , mNonPartialCount(0) michael@0: , mStatus(NS_OK) michael@0: , mIsPending(false) michael@0: , mDidOnStartRequest(false) michael@0: , mLastProgressUpdate(0) michael@0: , mRedirectCallback(nullptr) michael@0: , mNewRedirectChannel(nullptr) michael@0: , mCacheBust(false) michael@0: { michael@0: } michael@0: michael@0: nsresult michael@0: nsIncrementalDownload::FlushChunk() michael@0: { michael@0: NS_ASSERTION(mTotalSize != int64_t(-1), "total size should be known"); michael@0: michael@0: if (mChunkLen == 0) michael@0: return NS_OK; michael@0: michael@0: nsresult rv = AppendToFile(mDest, mChunk, mChunkLen); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mCurrentSize += int64_t(mChunkLen); michael@0: mChunkLen = 0; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsIncrementalDownload::UpdateProgress() michael@0: { michael@0: mLastProgressUpdate = PR_Now(); michael@0: michael@0: if (mProgressSink) michael@0: mProgressSink->OnProgress(this, mObserverContext, michael@0: uint64_t(int64_t(mCurrentSize) + mChunkLen), michael@0: uint64_t(int64_t(mTotalSize))); michael@0: } michael@0: michael@0: nsresult michael@0: nsIncrementalDownload::CallOnStartRequest() michael@0: { michael@0: if (!mObserver || mDidOnStartRequest) michael@0: return NS_OK; michael@0: michael@0: mDidOnStartRequest = true; michael@0: return mObserver->OnStartRequest(this, mObserverContext); michael@0: } michael@0: michael@0: void michael@0: nsIncrementalDownload::CallOnStopRequest() michael@0: { michael@0: if (!mObserver) michael@0: return; michael@0: michael@0: // Ensure that OnStartRequest is always called once before OnStopRequest. michael@0: nsresult rv = CallOnStartRequest(); michael@0: if (NS_SUCCEEDED(mStatus)) michael@0: mStatus = rv; michael@0: michael@0: mIsPending = false; michael@0: michael@0: mObserver->OnStopRequest(this, mObserverContext, mStatus); michael@0: mObserver = nullptr; michael@0: mObserverContext = nullptr; michael@0: } michael@0: michael@0: nsresult michael@0: nsIncrementalDownload::StartTimer(int32_t interval) michael@0: { michael@0: nsresult rv; michael@0: mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: return mTimer->Init(this, interval * 1000, nsITimer::TYPE_ONE_SHOT); michael@0: } michael@0: michael@0: nsresult michael@0: nsIncrementalDownload::ProcessTimeout() michael@0: { michael@0: NS_ASSERTION(!mChannel, "how can we have a channel?"); michael@0: michael@0: // Handle existing error conditions michael@0: if (NS_FAILED(mStatus)) { michael@0: CallOnStopRequest(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Fetch next chunk michael@0: michael@0: nsCOMPtr channel; michael@0: nsresult rv = NS_NewChannel(getter_AddRefs(channel), mFinalURI, nullptr, michael@0: nullptr, this, mLoadFlags); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: nsCOMPtr http = do_QueryInterface(channel, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: NS_ASSERTION(mCurrentSize != int64_t(-1), michael@0: "we should know the current file size by now"); michael@0: michael@0: rv = ClearRequestHeader(http); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Don't bother making a range request if we are just going to fetch the michael@0: // entire document. michael@0: if (mInterval || mCurrentSize != int64_t(0)) { michael@0: nsAutoCString range; michael@0: MakeRangeSpec(mCurrentSize, mTotalSize, mChunkSize, mInterval == 0, range); michael@0: michael@0: rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Range"), range, false); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: if (!mPartialValidator.IsEmpty()) michael@0: http->SetRequestHeader(NS_LITERAL_CSTRING("If-Range"), michael@0: mPartialValidator, false); michael@0: michael@0: if (mCacheBust) { michael@0: http->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"), michael@0: NS_LITERAL_CSTRING("no-cache"), false); michael@0: http->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"), michael@0: NS_LITERAL_CSTRING("no-cache"), false); michael@0: } michael@0: } michael@0: michael@0: rv = channel->AsyncOpen(this, nullptr); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Wait to assign mChannel when we know we are going to succeed. This is michael@0: // important because we don't want to introduce a reference cycle between michael@0: // mChannel and this until we know for a fact that AsyncOpen has succeeded, michael@0: // thus ensuring that our stream listener methods will be invoked. michael@0: mChannel = channel; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Reads the current file size and validates it. michael@0: nsresult michael@0: nsIncrementalDownload::ReadCurrentSize() michael@0: { michael@0: int64_t size; michael@0: nsresult rv = mDest->GetFileSize((int64_t *) &size); michael@0: if (rv == NS_ERROR_FILE_NOT_FOUND || michael@0: rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { michael@0: mCurrentSize = 0; michael@0: return NS_OK; michael@0: } michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mCurrentSize = size; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // nsISupports michael@0: michael@0: NS_IMPL_ISUPPORTS(nsIncrementalDownload, michael@0: nsIIncrementalDownload, michael@0: nsIRequest, michael@0: nsIStreamListener, michael@0: nsIRequestObserver, michael@0: nsIObserver, michael@0: nsIInterfaceRequestor, michael@0: nsIChannelEventSink, michael@0: nsISupportsWeakReference, michael@0: nsIAsyncVerifyRedirectCallback) michael@0: michael@0: // nsIRequest michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetName(nsACString &name) michael@0: { michael@0: NS_ENSURE_TRUE(mURI, NS_ERROR_NOT_INITIALIZED); michael@0: michael@0: return mURI->GetSpec(name); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::IsPending(bool *isPending) michael@0: { michael@0: *isPending = mIsPending; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetStatus(nsresult *status) michael@0: { michael@0: *status = mStatus; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Cancel(nsresult status) michael@0: { michael@0: NS_ENSURE_ARG(NS_FAILED(status)); michael@0: michael@0: // Ignore this cancelation if we're already canceled. michael@0: if (NS_FAILED(mStatus)) michael@0: return NS_OK; michael@0: michael@0: mStatus = status; michael@0: michael@0: // Nothing more to do if callbacks aren't pending. michael@0: if (!mIsPending) michael@0: return NS_OK; michael@0: michael@0: if (mChannel) { michael@0: mChannel->Cancel(mStatus); michael@0: NS_ASSERTION(!mTimer, "what is this timer object doing here?"); michael@0: } michael@0: else { michael@0: // dispatch a timer callback event to drive invoking our listener's michael@0: // OnStopRequest. michael@0: if (mTimer) michael@0: mTimer->Cancel(); michael@0: StartTimer(0); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Suspend() michael@0: { michael@0: return NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Resume() michael@0: { michael@0: return NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetLoadFlags(nsLoadFlags *loadFlags) michael@0: { michael@0: *loadFlags = mLoadFlags; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::SetLoadFlags(nsLoadFlags loadFlags) michael@0: { michael@0: mLoadFlags = loadFlags; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetLoadGroup(nsILoadGroup **loadGroup) michael@0: { michael@0: return NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::SetLoadGroup(nsILoadGroup *loadGroup) michael@0: { michael@0: return NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: // nsIIncrementalDownload michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Init(nsIURI *uri, nsIFile *dest, michael@0: int32_t chunkSize, int32_t interval) michael@0: { michael@0: // Keep it simple: only allow initialization once michael@0: NS_ENSURE_FALSE(mURI, NS_ERROR_ALREADY_INITIALIZED); michael@0: michael@0: mDest = do_QueryInterface(dest); michael@0: NS_ENSURE_ARG(mDest); michael@0: michael@0: mURI = uri; michael@0: mFinalURI = uri; michael@0: michael@0: if (chunkSize > 0) michael@0: mChunkSize = chunkSize; michael@0: if (interval >= 0) michael@0: mInterval = interval; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetURI(nsIURI **result) michael@0: { michael@0: NS_IF_ADDREF(*result = mURI); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetFinalURI(nsIURI **result) michael@0: { michael@0: NS_IF_ADDREF(*result = mFinalURI); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetDestination(nsIFile **result) michael@0: { michael@0: if (!mDest) { michael@0: *result = nullptr; michael@0: return NS_OK; michael@0: } michael@0: // Return a clone of mDest so that callers may modify the resulting nsIFile michael@0: // without corrupting our internal object. This also works around the fact michael@0: // that some nsIFile impls may cache the result of stat'ing the filesystem. michael@0: return mDest->Clone(result); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetTotalSize(int64_t *result) michael@0: { michael@0: *result = mTotalSize; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetCurrentSize(int64_t *result) michael@0: { michael@0: *result = mCurrentSize; michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Start(nsIRequestObserver *observer, michael@0: nsISupports *context) michael@0: { michael@0: NS_ENSURE_ARG(observer); michael@0: NS_ENSURE_FALSE(mIsPending, NS_ERROR_IN_PROGRESS); michael@0: michael@0: // Observe system shutdown so we can be sure to release any reference held michael@0: // between ourselves and the timer. We have the observer service hold a weak michael@0: // reference to us, so that we don't have to worry about calling michael@0: // RemoveObserver. XXX(darin): The timer code should do this for us. michael@0: nsCOMPtr obs = mozilla::services::GetObserverService(); michael@0: if (obs) michael@0: obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); michael@0: michael@0: nsresult rv = ReadCurrentSize(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = StartTimer(0); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: mObserver = observer; michael@0: mObserverContext = context; michael@0: mProgressSink = do_QueryInterface(observer); // ok if null michael@0: michael@0: mIsPending = true; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // nsIRequestObserver michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::OnStartRequest(nsIRequest *request, michael@0: nsISupports *context) michael@0: { michael@0: nsresult rv; michael@0: michael@0: nsCOMPtr http = do_QueryInterface(request, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // Ensure that we are receiving a 206 response. michael@0: uint32_t code; michael@0: rv = http->GetResponseStatus(&code); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: if (code != 206) { michael@0: // We may already have the entire file downloaded, in which case michael@0: // our request for a range beyond the end of the file would have michael@0: // been met with an error response code. michael@0: if (code == 416 && mTotalSize == int64_t(-1)) { michael@0: mTotalSize = mCurrentSize; michael@0: // Return an error code here to suppress OnDataAvailable. michael@0: return NS_ERROR_DOWNLOAD_COMPLETE; michael@0: } michael@0: // The server may have decided to give us all of the data in one chunk. If michael@0: // we requested a partial range, then we don't want to download all of the michael@0: // data at once. So, we'll just try again, but if this keeps happening then michael@0: // we'll eventually give up. michael@0: if (code == 200) { michael@0: if (mInterval) { michael@0: mChannel = nullptr; michael@0: if (++mNonPartialCount > MAX_RETRY_COUNT) { michael@0: NS_WARNING("unable to fetch a byte range; giving up"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: // Increase delay with each failure. michael@0: StartTimer(mInterval * mNonPartialCount); michael@0: return NS_ERROR_DOWNLOAD_NOT_PARTIAL; michael@0: } michael@0: // Since we have been asked to download the rest of the file, we can deal michael@0: // with a 200 response. This may result in downloading the beginning of michael@0: // the file again, but that can't really be helped. michael@0: } else { michael@0: NS_WARNING("server response was unexpected"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: } else { michael@0: // We got a partial response, so clear this counter in case the next chunk michael@0: // results in a 200 response. michael@0: mNonPartialCount = 0; michael@0: michael@0: // confirm that the content-range response header is consistent with michael@0: // expectations on each 206. If it is not then drop this response and michael@0: // retry with no-cache set. michael@0: if (!mCacheBust) { michael@0: nsAutoCString buf; michael@0: int64_t startByte = 0; michael@0: bool confirmedOK = false; michael@0: michael@0: rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; // it isn't a useful 206 without a CONTENT-RANGE of some sort michael@0: michael@0: // Content-Range: bytes 0-299999/25604694 michael@0: int32_t p = buf.Find("bytes "); michael@0: michael@0: // first look for the starting point of the content-range michael@0: // to make sure it is what we expect michael@0: if (p != -1) { michael@0: char *endptr = nullptr; michael@0: const char *s = buf.get() + p + 6; michael@0: while (*s && *s == ' ') michael@0: s++; michael@0: startByte = strtol(s, &endptr, 10); michael@0: michael@0: if (*s && endptr && (endptr != s) && michael@0: (mCurrentSize == startByte)) { michael@0: michael@0: // ok the starting point is confirmed. We still need to check the michael@0: // total size of the range for consistency if this isn't michael@0: // the first chunk michael@0: if (mTotalSize == int64_t(-1)) { michael@0: // first chunk michael@0: confirmedOK = true; michael@0: } else { michael@0: int32_t slash = buf.FindChar('/'); michael@0: int64_t rangeSize = 0; michael@0: if (slash != kNotFound && michael@0: (PR_sscanf(buf.get() + slash + 1, "%lld", (int64_t *) &rangeSize) == 1) && michael@0: rangeSize == mTotalSize) { michael@0: confirmedOK = true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!confirmedOK) { michael@0: NS_WARNING("unexpected content-range"); michael@0: mCacheBust = true; michael@0: mChannel = nullptr; michael@0: if (++mNonPartialCount > MAX_RETRY_COUNT) { michael@0: NS_WARNING("unable to fetch a byte range; giving up"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: // Increase delay with each failure. michael@0: StartTimer(mInterval * mNonPartialCount); michael@0: return NS_ERROR_DOWNLOAD_NOT_PARTIAL; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Do special processing after the first response. michael@0: if (mTotalSize == int64_t(-1)) { michael@0: // Update knowledge of mFinalURI michael@0: rv = http->GetURI(getter_AddRefs(mFinalURI)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: http->GetResponseHeader(NS_LITERAL_CSTRING("Etag"), mPartialValidator); michael@0: if (StringBeginsWith(mPartialValidator, NS_LITERAL_CSTRING("W/"))) michael@0: mPartialValidator.Truncate(); // don't use weak validators michael@0: if (mPartialValidator.IsEmpty()) michael@0: http->GetResponseHeader(NS_LITERAL_CSTRING("Last-Modified"), mPartialValidator); michael@0: michael@0: if (code == 206) { michael@0: // OK, read the Content-Range header to determine the total size of this michael@0: // download file. michael@0: nsAutoCString buf; michael@0: rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: int32_t slash = buf.FindChar('/'); michael@0: if (slash == kNotFound) { michael@0: NS_WARNING("server returned invalid Content-Range header!"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: if (PR_sscanf(buf.get() + slash + 1, "%lld", (int64_t *) &mTotalSize) != 1) michael@0: return NS_ERROR_UNEXPECTED; michael@0: } else { michael@0: rv = http->GetContentLength(&mTotalSize); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: // We need to know the total size of the thing we're trying to download. michael@0: if (mTotalSize == int64_t(-1)) { michael@0: NS_WARNING("server returned no content-length header!"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: // Need to truncate (or create, if it doesn't exist) the file since we michael@0: // are downloading the whole thing. michael@0: WriteToFile(mDest, nullptr, 0, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE); michael@0: mCurrentSize = 0; michael@0: } michael@0: michael@0: // Notify observer that we are starting... michael@0: rv = CallOnStartRequest(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: michael@0: // Adjust mChunkSize accordingly if mCurrentSize is close to mTotalSize. michael@0: int64_t diff = mTotalSize - mCurrentSize; michael@0: if (diff <= int64_t(0)) { michael@0: NS_WARNING("about to set a bogus chunk size; giving up"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: if (diff < int64_t(mChunkSize)) michael@0: mChunkSize = uint32_t(diff); michael@0: michael@0: mChunk = new char[mChunkSize]; michael@0: if (!mChunk) michael@0: rv = NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::OnStopRequest(nsIRequest *request, michael@0: nsISupports *context, michael@0: nsresult status) michael@0: { michael@0: // Not a real error; just a trick to kill off the channel without our michael@0: // listener having to care. michael@0: if (status == NS_ERROR_DOWNLOAD_NOT_PARTIAL) michael@0: return NS_OK; michael@0: michael@0: // Not a real error; just a trick used to suppress OnDataAvailable calls. michael@0: if (status == NS_ERROR_DOWNLOAD_COMPLETE) michael@0: status = NS_OK; michael@0: michael@0: if (NS_SUCCEEDED(mStatus)) michael@0: mStatus = status; michael@0: michael@0: if (mChunk) { michael@0: if (NS_SUCCEEDED(mStatus)) michael@0: mStatus = FlushChunk(); michael@0: michael@0: mChunk = nullptr; // deletes memory michael@0: mChunkLen = 0; michael@0: UpdateProgress(); michael@0: } michael@0: michael@0: mChannel = nullptr; michael@0: michael@0: // Notify listener if we hit an error or finished michael@0: if (NS_FAILED(mStatus) || mCurrentSize == mTotalSize) { michael@0: CallOnStopRequest(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: return StartTimer(mInterval); // Do next chunk michael@0: } michael@0: michael@0: // nsIStreamListener michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::OnDataAvailable(nsIRequest *request, michael@0: nsISupports *context, michael@0: nsIInputStream *input, michael@0: uint64_t offset, michael@0: uint32_t count) michael@0: { michael@0: while (count) { michael@0: uint32_t space = mChunkSize - mChunkLen; michael@0: uint32_t n, len = std::min(space, count); michael@0: michael@0: nsresult rv = input->Read(mChunk + mChunkLen, len, &n); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: if (n != len) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: count -= n; michael@0: mChunkLen += n; michael@0: michael@0: if (mChunkLen == mChunkSize) { michael@0: rv = FlushChunk(); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: } michael@0: michael@0: if (PR_Now() > mLastProgressUpdate + UPDATE_PROGRESS_INTERVAL) michael@0: UpdateProgress(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: // nsIObserver michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::Observe(nsISupports *subject, const char *topic, michael@0: const char16_t *data) michael@0: { michael@0: if (strcmp(topic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { michael@0: Cancel(NS_ERROR_ABORT); michael@0: michael@0: // Since the app is shutting down, we need to go ahead and notify our michael@0: // observer here. Otherwise, we would notify them after XPCOM has been michael@0: // shutdown or not at all. michael@0: CallOnStopRequest(); michael@0: } michael@0: else if (strcmp(topic, NS_TIMER_CALLBACK_TOPIC) == 0) { michael@0: mTimer = nullptr; michael@0: nsresult rv = ProcessTimeout(); michael@0: if (NS_FAILED(rv)) michael@0: Cancel(rv); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: // nsIInterfaceRequestor michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::GetInterface(const nsIID &iid, void **result) michael@0: { michael@0: if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) { michael@0: NS_ADDREF_THIS(); michael@0: *result = static_cast(this); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsCOMPtr ir = do_QueryInterface(mObserver); michael@0: if (ir) michael@0: return ir->GetInterface(iid, result); michael@0: michael@0: return NS_ERROR_NO_INTERFACE; michael@0: } michael@0: michael@0: nsresult michael@0: nsIncrementalDownload::ClearRequestHeader(nsIHttpChannel *channel) michael@0: { michael@0: NS_ENSURE_ARG(channel); michael@0: michael@0: // We don't support encodings -- they make the Content-Length not equal michael@0: // to the actual size of the data. michael@0: return channel->SetRequestHeader(NS_LITERAL_CSTRING("Accept-Encoding"), michael@0: NS_LITERAL_CSTRING(""), false); michael@0: } michael@0: michael@0: // nsIChannelEventSink michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::AsyncOnChannelRedirect(nsIChannel *oldChannel, michael@0: nsIChannel *newChannel, michael@0: uint32_t flags, michael@0: nsIAsyncVerifyRedirectCallback *cb) michael@0: { michael@0: // In response to a redirect, we need to propagate the Range header. See bug michael@0: // 311595. Any failure code returned from this function aborts the redirect. michael@0: michael@0: nsCOMPtr http = do_QueryInterface(oldChannel); michael@0: NS_ENSURE_STATE(http); michael@0: michael@0: nsCOMPtr newHttpChannel = do_QueryInterface(newChannel); michael@0: NS_ENSURE_STATE(newHttpChannel); michael@0: michael@0: NS_NAMED_LITERAL_CSTRING(rangeHdr, "Range"); michael@0: michael@0: nsresult rv = ClearRequestHeader(newHttpChannel); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: // If we didn't have a Range header, then we must be doing a full download. michael@0: nsAutoCString rangeVal; michael@0: http->GetRequestHeader(rangeHdr, rangeVal); michael@0: if (!rangeVal.IsEmpty()) { michael@0: rv = newHttpChannel->SetRequestHeader(rangeHdr, rangeVal, false); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: // A redirection changes the validator michael@0: mPartialValidator.Truncate(); michael@0: michael@0: if (mCacheBust) { michael@0: newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"), michael@0: NS_LITERAL_CSTRING("no-cache"), false); michael@0: newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"), michael@0: NS_LITERAL_CSTRING("no-cache"), false); michael@0: } michael@0: michael@0: // Prepare to receive callback michael@0: mRedirectCallback = cb; michael@0: mNewRedirectChannel = newChannel; michael@0: michael@0: // Give the observer a chance to see this redirect notification. michael@0: nsCOMPtr sink = do_GetInterface(mObserver); michael@0: if (sink) { michael@0: rv = sink->AsyncOnChannelRedirect(oldChannel, newChannel, flags, this); michael@0: if (NS_FAILED(rv)) { michael@0: mRedirectCallback = nullptr; michael@0: mNewRedirectChannel = nullptr; michael@0: } michael@0: return rv; michael@0: } michael@0: (void) OnRedirectVerifyCallback(NS_OK); michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsIncrementalDownload::OnRedirectVerifyCallback(nsresult result) michael@0: { michael@0: NS_ASSERTION(mRedirectCallback, "mRedirectCallback not set in callback"); michael@0: NS_ASSERTION(mNewRedirectChannel, "mNewRedirectChannel not set in callback"); michael@0: michael@0: // Update mChannel, so we can Cancel the new channel. michael@0: if (NS_SUCCEEDED(result)) michael@0: mChannel = mNewRedirectChannel; michael@0: michael@0: mRedirectCallback->OnRedirectVerifyCallback(result); michael@0: mRedirectCallback = nullptr; michael@0: mNewRedirectChannel = nullptr; michael@0: return NS_OK; michael@0: } michael@0: michael@0: extern nsresult michael@0: net_NewIncrementalDownload(nsISupports *outer, const nsIID &iid, void **result) michael@0: { michael@0: if (outer) michael@0: return NS_ERROR_NO_AGGREGATION; michael@0: michael@0: nsIncrementalDownload *d = new nsIncrementalDownload(); michael@0: if (!d) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: NS_ADDREF(d); michael@0: nsresult rv = d->QueryInterface(iid, result); michael@0: NS_RELEASE(d); michael@0: return rv; michael@0: }