bug 816472 - incremental downloader error checking around bad server byte ranges r=jduell
authorPatrick McManus <mcmanus@ducksong.com>
Sat, 19 Oct 2013 21:08:25 -0400
changeset 166224 cbf2fa780040cc1a243f5fd37250fc7d83bfa50f
parent 166223 188f870378573956b1b3a8cb6a9407d92f49e8e8
child 166225 2a0c1a40594f123d5978e4a2349a03887f45a879
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjduell
bugs816472
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 816472 - incremental downloader error checking around bad server byte ranges r=jduell
netwerk/base/src/nsIncrementalDownload.cpp
--- a/netwerk/base/src/nsIncrementalDownload.cpp
+++ b/netwerk/base/src/nsIncrementalDownload.cpp
@@ -151,32 +151,35 @@ private:
   uint32_t                                 mLoadFlags;
   int32_t                                  mNonPartialCount;
   nsresult                                 mStatus;
   bool                                     mIsPending;
   bool                                     mDidOnStartRequest;
   PRTime                                   mLastProgressUpdate;
   nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
   nsCOMPtr<nsIChannel>                     mNewRedirectChannel;
+  nsCString                                mPartialValidator;
+  bool                                     mCacheBust;
 };
 
 nsIncrementalDownload::nsIncrementalDownload()
   : mChunkLen(0)
   , mChunkSize(DEFAULT_CHUNK_SIZE)
   , mInterval(DEFAULT_INTERVAL)
   , mTotalSize(-1)
   , mCurrentSize(-1)
   , mLoadFlags(LOAD_NORMAL)
   , mNonPartialCount(0)
   , mStatus(NS_OK)
   , mIsPending(false)
   , mDidOnStartRequest(false)
   , mLastProgressUpdate(0)
   , mRedirectCallback(nullptr)
   , mNewRedirectChannel(nullptr)
+  , mCacheBust(false)  
 {
 }
 
 nsresult
 nsIncrementalDownload::FlushChunk()
 {
   NS_ASSERTION(mTotalSize != int64_t(-1), "total size should be known");
 
@@ -277,16 +280,27 @@ nsIncrementalDownload::ProcessTimeout()
   // entire document.
   if (mInterval || mCurrentSize != int64_t(0)) {
     nsAutoCString range;
     MakeRangeSpec(mCurrentSize, mTotalSize, mChunkSize, mInterval == 0, range);
 
     rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Range"), range, false);
     if (NS_FAILED(rv))
       return rv;
+
+    if (!mPartialValidator.IsEmpty())
+      http->SetRequestHeader(NS_LITERAL_CSTRING("If-Range"),
+                             mPartialValidator, false);
+
+    if (mCacheBust) {
+      http->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"),
+                             NS_LITERAL_CSTRING("no-cache"), false);
+      http->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"),
+                             NS_LITERAL_CSTRING("no-cache"), false);
+    }
   }
 
   rv = channel->AsyncOpen(this, nullptr);
   if (NS_FAILED(rv))
     return rv;
 
   // Wait to assign mChannel when we know we are going to succeed.  This is
   // important because we don't want to introduce a reference cycle between
@@ -560,24 +574,88 @@ nsIncrementalDownload::OnStartRequest(ns
     } else {
       NS_WARNING("server response was unexpected");
       return NS_ERROR_UNEXPECTED;
     }
   } else {
     // We got a partial response, so clear this counter in case the next chunk
     // results in a 200 response.
     mNonPartialCount = 0;
+
+    // confirm that the content-range response header is consistent with
+    // expectations on each 206. If it is not then drop this response and
+    // retry with no-cache set.
+    if (!mCacheBust) {
+      nsAutoCString buf;
+      int64_t startByte = 0;
+      bool confirmedOK = false;
+
+      rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf);
+      if (NS_FAILED(rv))
+        return rv; // it isn't a useful 206 without a CONTENT-RANGE of some sort
+
+      // Content-Range: bytes 0-299999/25604694
+      int32_t p = buf.Find("bytes ");
+
+      // first look for the starting point of the content-range
+      // to make sure it is what we expect
+      if (p != -1) {
+        char *endptr = nullptr;
+        const char *s = buf.get() + p + 6;
+        while (*s && *s == ' ')
+          s++;
+        startByte = strtol(s, &endptr, 10);
+
+        if (*s && endptr && (endptr != s) &&
+            (mCurrentSize == startByte)) {
+
+          // ok the starting point is confirmed. We still need to check the
+          // total size of the range for consistency if this isn't
+          // the first chunk
+          if (mTotalSize == int64_t(-1)) {
+            // first chunk
+            confirmedOK = true;
+          } else {
+            int32_t slash = buf.FindChar('/');
+            int64_t rangeSize = 0;
+            if (slash != kNotFound &&
+                (PR_sscanf(buf.get() + slash + 1, "%lld", (int64_t *) &rangeSize) == 1) &&
+                rangeSize == mTotalSize) {
+              confirmedOK = true;
+            }
+          }
+        }
+      }
+
+      if (!confirmedOK) {
+        NS_WARNING("unexpected content-range");
+        mCacheBust = true;
+        mChannel = nullptr;
+        if (++mNonPartialCount > MAX_RETRY_COUNT) {
+          NS_WARNING("unable to fetch a byte range; giving up");
+          return NS_ERROR_FAILURE;
+        }
+        // Increase delay with each failure.
+        StartTimer(mInterval * mNonPartialCount);
+        return NS_ERROR_DOWNLOAD_NOT_PARTIAL;
+      }
+    }
   }
 
   // Do special processing after the first response.
   if (mTotalSize == int64_t(-1)) {
     // Update knowledge of mFinalURI
     rv = http->GetURI(getter_AddRefs(mFinalURI));
     if (NS_FAILED(rv))
       return rv;
+    http->GetResponseHeader(NS_LITERAL_CSTRING("Etag"), mPartialValidator);
+    if (StringBeginsWith(mPartialValidator, NS_LITERAL_CSTRING("W/")))
+      mPartialValidator.Truncate(); // don't use weak validators
+    if (mPartialValidator.IsEmpty())
+      http->GetResponseHeader(NS_LITERAL_CSTRING("Last-Modified"), mPartialValidator);
 
     if (code == 206) {
       // OK, read the Content-Range header to determine the total size of this
       // download file.
       nsAutoCString buf;
       rv = http->GetResponseHeader(NS_LITERAL_CSTRING("Content-Range"), buf);
       if (NS_FAILED(rv))
         return rv;
@@ -773,16 +851,26 @@ nsIncrementalDownload::AsyncOnChannelRed
   // If we didn't have a Range header, then we must be doing a full download.
   nsAutoCString rangeVal;
   http->GetRequestHeader(rangeHdr, rangeVal);
   if (!rangeVal.IsEmpty()) {
     rv = newHttpChannel->SetRequestHeader(rangeHdr, rangeVal, false);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
+  // A redirection changes the validator
+  mPartialValidator.Truncate();
+
+  if (mCacheBust) {
+    newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Cache-Control"),
+                                     NS_LITERAL_CSTRING("no-cache"), false);
+    newHttpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Pragma"),
+                                     NS_LITERAL_CSTRING("no-cache"), false);
+  }
+
   // Prepare to receive callback
   mRedirectCallback = cb;
   mNewRedirectChannel = newChannel;
 
   // Give the observer a chance to see this redirect notification.
   nsCOMPtr<nsIChannelEventSink> sink = do_GetInterface(mObserver);
   if (sink) {
     rv = sink->AsyncOnChannelRedirect(oldChannel, newChannel, flags, this);