From 947b301aa863cfe299ebb35af0a344576e7c7568 Mon Sep 17 00:00:00 2001 From: Patrick McManus Date: Tue, 17 Sep 2013 18:44:03 -0400 Subject: [PATCH] bug 911325 - detect mismatch between 206 content-range and 200 content-length r=jduell --- netwerk/protocol/http/nsHttpChannel.cpp | 24 ++++- netwerk/test/unit/test_range_requests.js | 120 ++++++++++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 854ce82d073..5dec490115f 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -2194,6 +2194,25 @@ nsHttpChannel::ProcessPartialContent() } + int64_t cachedContentLength = mCachedResponseHead->ContentLength(); + int64_t entitySize = mResponseHead->TotalEntitySize(); + + LOG(("nsHttpChannel::ProcessPartialContent [this=%p trans=%p] " + "original content-length %lld, entity-size %lld, content-range %s\n", + this, mTransaction.get(), cachedContentLength, entitySize, + mResponseHead->PeekHeader(nsHttp::Content_Range))); + + if ((entitySize >= 0) && (cachedContentLength >= 0) && + (entitySize != cachedContentLength)) { + LOG(("nsHttpChannel::ProcessPartialContent [this=%p] " + "206 has different total entity size than the content length " + "of the original partially cached entity.\n", this)); + + mCacheEntry->AsyncDoom(nullptr); + Cancel(NS_ERROR_CORRUPTED_CONTENT); + return CallOnStartRequest(); + } + // suspend the current transaction nsresult rv = mTransactionPump->Suspend(); if (NS_FAILED(rv)) return rv; @@ -5327,7 +5346,10 @@ nsHttpChannel::OnDataAvailable(nsIRequest *request, nsISupports *ctxt, uint64_t progressMax(uint64_t(mResponseHead->ContentLength())); uint64_t progress = mLogicalOffset + uint64_t(count); - MOZ_ASSERT(progress <= progressMax, "unexpected progress values"); + + if (progress > progressMax) + NS_WARNING("unexpected progress values - " + "is server exceeding content length?"); if (NS_IsMainThread()) { OnTransportStatus(nullptr, transportStatus, progress, progressMax); diff --git a/netwerk/test/unit/test_range_requests.js b/netwerk/test/unit/test_range_requests.js index 5cb51183d92..42237f867aa 100644 --- a/netwerk/test/unit/test_range_requests.js +++ b/netwerk/test/unit/test_range_requests.js @@ -11,6 +11,9 @@ // 4) the cached entry does not have a Content-Encoding (see bug #613159) // 5) the request does not have a conditional-request header set by client // 6) nsHttpResponseHead::IsResumable() is true for the cached entry +// 7) a basic positive test that makes sure byte ranges work +// 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size +// of 206 does not match content-length of 200 // // The test has one handler for each case and run_tests() fires one request // for each. None of the handlers should see a Range-header. @@ -88,6 +91,28 @@ MyListener.prototype = { } }; + +function FailedChannelListener(continueFn) { + this.continueFn = continueFn; +} +FailedChannelListener.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { }, + + onDataAvailable: function(request, context, stream, offset, count) { }, + + onStopRequest: function(request, context, status) { + do_check_eq(status, Components.results.NS_ERROR_CORRUPTED_CONTENT); + this.continueFn(request, null); + } +}; + function received_cleartext(request, data) { do_check_eq(clearTextBody, data); testFinished(); @@ -231,10 +256,93 @@ function received_partial_6(request, data) { chan.asyncOpen(new ChannelListener(received_cleartext, null), null); } +const simpleBody = "0123456789"; + +function received_simple(request, data) { + do_check_eq(simpleBody, data); + testFinished(); +} + +var case_7_request_no = 0; +function handler_7(metadata, response) { + switch (case_7_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test7Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); + response.finish(); + break; + case 1: + do_check_true(metadata.hasHeader("Range")); + do_check_true(metadata.hasHeader("If-Range")); + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test7Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "6"); + response.setHeader("Content-Range", "4-9/10"); + response.bodyOutputStream.write(simpleBody.slice(4), 6); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_7_request_no++; +} +function received_partial_7(request, data) { + // make sure we get the first 4 bytes + do_check_eq(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen(new ChannelListener(received_simple, null), null); +} + +var case_8_request_no = 0; +function handler_8(metadata, response) { + switch (case_8_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test8Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Cache-Control", "max-age=360000"); + response.setHeader("Content-Length", "10"); + response.processAsync(); + response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); + response.finish(); + break; + case 1: + do_check_true(metadata.hasHeader("Range")); + do_check_true(metadata.hasHeader("If-Range")); + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "test8Etag"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "5"); + response.setHeader("Content-Range", "4-8/9"); // intentionally broken + response.bodyOutputStream.write(simpleBody.slice(4), 5); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_8_request_no++; +} +function received_partial_8(request, data) { + // make sure we get the first 4 bytes + do_check_eq(4, data.length); + // do it again to get the rest + var chan = make_channel("http://localhost:" + port + "/test_8"); + chan.asyncOpen(new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE), null); +} + // Simple mechanism to keep track of tests and stop the server var numTestsFinished = 0; function testFinished() { - if (++numTestsFinished == 5) + if (++numTestsFinished == 7) httpserver.stop(do_test_finished); } @@ -245,6 +353,8 @@ function run_test() { httpserver.registerPathHandler("/test_4", handler_4); httpserver.registerPathHandler("/test_5", handler_5); httpserver.registerPathHandler("/test_6", handler_6); + httpserver.registerPathHandler("/test_7", handler_7); + httpserver.registerPathHandler("/test_8", handler_8); httpserver.start(-1); port = httpserver.identity.primaryPort; @@ -272,5 +382,13 @@ function run_test() { var chan = make_channel("http://localhost:" + port + "/test_6"); chan.asyncOpen(new MyListener(received_partial_6), null); + // Case 7: a basic positive test + var chan = make_channel("http://localhost:" + port + "/test_7"); + chan.asyncOpen(new MyListener(received_partial_7), null); + + // Case 8: check that mismatched 206 and 200 sizes throw error + var chan = make_channel("http://localhost:" + port + "/test_8"); + chan.asyncOpen(new MyListener(received_partial_8), null); + do_test_pending(); }