Bug 462549: Verify that an appcache manifest hasn't changed at the end of an update. r+sr=biesi

This commit is contained in:
Dave Camp 2008-11-05 16:01:07 -08:00
parent c27c345970
commit 19d2223559
8 changed files with 390 additions and 23 deletions

View File

@ -56,6 +56,7 @@ _TEST_FILES = \
test_simpleManifest.html \
test_identicalManifest.html \
test_changingManifest.html \
test_refetchManifest.html \
test_offlineIFrame.html \
test_bug445544.html \
445544_part1.html \
@ -88,7 +89,8 @@ _TEST_FILES = \
simpleManifest.notmanifest \
changing1Sec.sjs \
changing1Hour.sjs \
changingManifest.sjs \
changingManifest.cacheManifest \
changingManifest.cacheManifest^headers^ \
offlineChild.html \
$(NULL)

View File

@ -0,0 +1,5 @@
CACHE MANIFEST
# v1 - this will be replaced by test scripts.
changing1Hour.sjs
changing1Sec.sjs

View File

@ -0,0 +1,2 @@
Content-Type: text/cache-manifest
Cache-Control: no-cache

View File

@ -1,12 +0,0 @@
function handleRequest(request, response)
{
response.setStatusLine(request.httpVersion, 200, "Ok");
response.setHeader("Content-Type", "text/cache-manifest");
response.setHeader("Cache-Control", "no-cache");
response.write("CACHE MANIFEST\n");
response.write("#" + Date.now() + "\n");
response.write("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Hour.sjs\n");
response.write("http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changing1Sec.sjs\n");
}

View File

@ -1,4 +1,4 @@
<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.sjs">
<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest">
<head>
<title>changing manifest test</title>
@ -17,6 +17,17 @@ var g1HourUrl = "http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/ch
var gCacheContents = null;
function finish()
{
// Get rid of the changed manifest.
var req = new XMLHttpRequest();
req.open("DELETE", "changingManifest.cacheManifest", false);
req.send("");
OfflineTest.teardown();
OfflineTest.finish();
}
function manifestUpdatedAgain()
{
OfflineTest.ok(gGotChecking, "Should get a checking event on the second update");
@ -30,15 +41,13 @@ function manifestUpdatedAgain()
OfflineTest.isnot(gCacheContents[g1SecUrl], contents[g1SecUrl], "1-second expiration should have changed");
OfflineTest.is(gCacheContents[g1HourUrl], contents[g1HourUrl], "1-hour expiration should not have changed");
OfflineTest.teardown();
OfflineTest.finish();
finish();
});
}
function failAndFinish(e) {
OfflineTest.ok(false, "Unexpected event: " + e.type);
OfflineTest.teardown();
OfflineTest.finish();
finish();
}
function manifestUpdated()
@ -46,12 +55,25 @@ function manifestUpdated()
OfflineTest.ok(gGotChecking, "Should get a checking event");
OfflineTest.ok(gGotDownloading, "Should get a downloading event");
// Replace this manifest with a new one.
// XXX: After this put, we will no longer have Cache-Control:
// no-cache on the manifest, so future updates will just use the
// cached manifest.
var req = new XMLHttpRequest();
req.open("PUT", "changingManifest.cacheManifest", false);
req.setRequestHeader("Content-Type", "text/cache-manifest");
req.send("CACHE MANIFEST\n" +
"# v2\n" +
"changing1Hour.sjs\n" +
"changing1Sec.sjs\n");
// Get the initial contents of the first two files.
fetcher = new OfflineCacheContents([g1SecUrl, g1HourUrl]);
fetcher.fetch(function(contents) {
gCacheContents = contents;
// Now make sure applicationCache.update() does what we expect.
// Now make sure applicationCache.update() does what we expect.
applicationCache.onupdateready = OfflineTest.priv(manifestUpdatedAgain);
applicationCache.onnoupdate = failAndFinish;
applicationCache.oncached = failAndFinish;

View File

@ -0,0 +1,103 @@
<html xmlns="http://www.w3.org/1999/xhtml" manifest="http://localhost:8888/tests/dom/tests/mochitest/ajax/offline/changingManifest.cacheManifest">
<head>
<title>refetch manifest test</title>
<script type="text/javascript" src="/MochiKit/packed.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="/tests/dom/tests/mochitest/ajax/offline/offlineTests.js"></script>
<script type="text/javascript">
function finish()
{
// Get rid of the changed manifest.
var req = new XMLHttpRequest();
req.open("DELETE", "changingManifest.cacheManifest", false);
req.send("");
OfflineTest.teardown();
OfflineTest.finish();
}
function failAndFinish(e)
{
OfflineTest.ok(false, "Unexpected event: " + e.type);
finish();
}
function manifestUpdated()
{
// Replace this manifest with a new one.
// XXX: After this put, we will no longer have Cache-Control:
// no-cache on the manifest, so future updates will just use the
// cached manifest.
// Get the initial contents of the first two files.
fetcher = new OfflineCacheContents([g1SecUrl, g1HourUrl]);
fetcher.fetch(function(contents) {
gCacheContents = contents;
// Now make sure applicationCache.update() does what we expect.
applicationCache.onupdateready = OfflineTest.priv(manifestUpdatedAgain);
applicationCache.onnoupdate = failAndFinish;
applicationCache.oncached = failAndFinish;
gGotChecking = false;
gGotDownloading = false;
// The changing versions give out a new version each second,
// make sure it has time to grab a new version, and for the
// 1-second cache timeout to pass.
window.setTimeout("applicationCache.update()", 5000);
});
}
function replaceManifest()
{
// If we replace the manifest after a downloading event, the update
// should fail when it revalidates the manifest at the end of the update.
var req = new XMLHttpRequest();
req.open("PUT", "changingManifest.cacheManifest", false);
req.setRequestHeader("Content-Type", "text/cache-manifest");
req.send("CACHE MANIFEST\n" +
"# v2\n" +
"changing1Hour.sjs\n" +
"changing1Sec.sjs\n");
}
function cached()
{
OfflineTest.ok(true, "Got the expected cached event.");
finish();
}
function gotError()
{
OfflineTest.ok(true, "Got the expected error event.");
// Now this update will be rescheduled, and it should succeed.
applicationCache.onerror = failAndFinish;
applicationCache.oncached = cached;
}
if (OfflineTest.setup()) {
applicationCache.onerror = gotError;
applicationCache.onnoupdate = failAndFinish;
applicationCache.ondownloading = replaceManifest;
applicationCache.oncached = failAndFinish;
}
SimpleTest.waitForExplicitFinish();
</script>
</head>
<body>
</body>
</html>

View File

@ -68,6 +68,8 @@
static nsOfflineCacheUpdateService *gOfflineCacheUpdateService = nsnull;
static const PRUint32 kRescheduleLimit = 3;
#if defined(PR_LOGGING)
//
// To enable logging (see prlog.h for full details):
@ -105,6 +107,188 @@ DropReferenceFromURL(nsIURI * aURI)
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsManifestCheck
//-----------------------------------------------------------------------------
class nsManifestCheck : public nsIStreamListener
, public nsIChannelEventSink
, public nsIInterfaceRequestor
{
public:
nsManifestCheck(nsOfflineCacheUpdate *aUpdate,
nsIURI *aURI,
nsIURI *aReferrerURI)
: mUpdate(aUpdate)
, mURI(aURI)
, mReferrerURI(aReferrerURI)
{}
NS_DECL_ISUPPORTS
NS_DECL_NSIREQUESTOBSERVER
NS_DECL_NSISTREAMLISTENER
NS_DECL_NSICHANNELEVENTSINK
NS_DECL_NSIINTERFACEREQUESTOR
nsresult Begin();
private:
static NS_METHOD ReadManifest(nsIInputStream *aInputStream,
void *aClosure,
const char *aFromSegment,
PRUint32 aOffset,
PRUint32 aCount,
PRUint32 *aBytesConsumed);
nsRefPtr<nsOfflineCacheUpdate> mUpdate;
nsCOMPtr<nsIURI> mURI;
nsCOMPtr<nsIURI> mReferrerURI;
nsCOMPtr<nsICryptoHash> mManifestHash;
nsCOMPtr<nsIChannel> mChannel;
};
//-----------------------------------------------------------------------------
// nsManifestCheck::nsISupports
//-----------------------------------------------------------------------------
NS_IMPL_ISUPPORTS4(nsManifestCheck,
nsIRequestObserver,
nsIStreamListener,
nsIChannelEventSink,
nsIInterfaceRequestor)
//-----------------------------------------------------------------------------
// nsManifestCheck <public>
//-----------------------------------------------------------------------------
nsresult
nsManifestCheck::Begin()
{
nsresult rv;
mManifestHash = do_CreateInstance("@mozilla.org/security/hash;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
rv = mManifestHash->Init(nsICryptoHash::MD5);
NS_ENSURE_SUCCESS(rv, rv);
rv = NS_NewChannel(getter_AddRefs(mChannel),
mURI,
nsnull, nsnull, nsnull,
nsIRequest::LOAD_BYPASS_CACHE);
NS_ENSURE_SUCCESS(rv, rv);
// configure HTTP specific stuff
nsCOMPtr<nsIHttpChannel> httpChannel =
do_QueryInterface(mChannel);
if (httpChannel) {
httpChannel->SetReferrer(mReferrerURI);
httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("X-Moz"),
NS_LITERAL_CSTRING("offline-resource"),
PR_FALSE);
}
rv = mChannel->AsyncOpen(this, nsnull);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsManifestCheck <public>
//-----------------------------------------------------------------------------
/* static */
NS_METHOD
nsManifestCheck::ReadManifest(nsIInputStream *aInputStream,
void *aClosure,
const char *aFromSegment,
PRUint32 aOffset,
PRUint32 aCount,
PRUint32 *aBytesConsumed)
{
nsManifestCheck *manifestCheck =
static_cast<nsManifestCheck*>(aClosure);
nsresult rv;
*aBytesConsumed = aCount;
rv = manifestCheck->mManifestHash->Update(
reinterpret_cast<const PRUint8 *>(aFromSegment), aCount);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsManifestCheck::nsIStreamListener
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsManifestCheck::OnStartRequest(nsIRequest *aRequest,
nsISupports *aContext)
{
return NS_OK;
}
NS_IMETHODIMP
nsManifestCheck::OnDataAvailable(nsIRequest *aRequest,
nsISupports *aContext,
nsIInputStream *aStream,
PRUint32 aOffset,
PRUint32 aCount)
{
PRUint32 bytesRead;
aStream->ReadSegments(ReadManifest, this, aCount, &bytesRead);
return NS_OK;
}
NS_IMETHODIMP
nsManifestCheck::OnStopRequest(nsIRequest *aRequest,
nsISupports *aContext,
nsresult aStatus)
{
nsCAutoString manifestHash;
if (NS_SUCCEEDED(aStatus)) {
mManifestHash->Finish(PR_TRUE, manifestHash);
}
mUpdate->ManifestCheckCompleted(aStatus, manifestHash);
return NS_OK;
}
//-----------------------------------------------------------------------------
// nsManifestCheck::nsIInterfaceRequestor
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsManifestCheck::GetInterface(const nsIID &aIID, void **aResult)
{
if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) {
NS_ADDREF_THIS();
*aResult = static_cast<nsIChannelEventSink *>(this);
return NS_OK;
}
return NS_ERROR_NO_INTERFACE;
}
//-----------------------------------------------------------------------------
// nsManifestCheck::nsIChannelEventSink
//-----------------------------------------------------------------------------
NS_IMETHODIMP
nsManifestCheck::OnChannelRedirect(nsIChannel *aOldChannel,
nsIChannel *aNewChannel,
PRUint32 aFlags)
{
// Redirects should cause the load (and therefore the update) to fail.
if (aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)
return NS_OK;
aOldChannel->Cancel(NS_ERROR_ABORT);
return NS_ERROR_ABORT;
}
//-----------------------------------------------------------------------------
// nsOfflineCacheUpdateItem::nsISupports
//-----------------------------------------------------------------------------
@ -754,7 +938,7 @@ nsOfflineManifestItem::CheckNewManifestContentHash(nsIRequest *aRequest)
}
nsCString newManifestHashValue;
rv = mManifestHash->Finish(PR_TRUE, newManifestHashValue);
rv = mManifestHash->Finish(PR_TRUE, mManifestHashValue);
mManifestHash = nsnull;
if (NS_FAILED(rv)) {
@ -768,7 +952,7 @@ nsOfflineManifestItem::CheckNewManifestContentHash(nsIRequest *aRequest)
return NS_OK;
}
if (mOldManifestHashValue == newManifestHashValue) {
if (mOldManifestHashValue == mManifestHashValue) {
LOG(("Update not needed, downloaded manifest content is byte-for-byte identical"));
mNeedsUpdate = PR_FALSE;
}
@ -784,7 +968,7 @@ nsOfflineManifestItem::CheckNewManifestContentHash(nsIRequest *aRequest)
nsCOMPtr<nsICacheEntryDescriptor> cacheDescriptor(do_QueryInterface(cacheToken, &rv));
NS_ENSURE_SUCCESS(rv, rv);
rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", PromiseFlatCString(newManifestHashValue).get());
rv = cacheDescriptor->SetMetaDataElement("offline-manifest-hash", mManifestHashValue.get());
NS_ENSURE_SUCCESS(rv, rv);
}
@ -904,6 +1088,7 @@ nsOfflineCacheUpdate::nsOfflineCacheUpdate()
, mSucceeded(PR_TRUE)
, mObsolete(PR_FALSE)
, mCurrentItem(-1)
, mRescheduleCount(0)
{
}
@ -1199,6 +1384,40 @@ nsOfflineCacheUpdate::LoadCompleted()
ProcessNextURI();
}
void
nsOfflineCacheUpdate::ManifestCheckCompleted(nsresult aStatus,
const nsCString &aManifestHash)
{
if (NS_SUCCEEDED(aStatus)) {
nsCAutoString firstManifestHash;
mManifestItem->GetManifestHash(firstManifestHash);
if (aManifestHash != firstManifestHash) {
aStatus = NS_ERROR_FAILURE;
}
}
if (NS_FAILED(aStatus)) {
mSucceeded = PR_FALSE;
NotifyError();
}
Finish();
if (NS_FAILED(aStatus) && mRescheduleCount < kRescheduleLimit) {
// Reschedule this update.
nsRefPtr<nsOfflineCacheUpdate> newUpdate =
new nsOfflineCacheUpdate();
newUpdate->Init(mManifestURI, mDocumentURI);
for (PRInt32 i = 0; i < mDocuments.Count(); i++) {
newUpdate->AddDocument(mDocuments[i]);
}
newUpdate->mRescheduleCount = mRescheduleCount + 1;
newUpdate->Schedule();
}
}
nsresult
nsOfflineCacheUpdate::Begin()
{
@ -1310,7 +1529,23 @@ nsOfflineCacheUpdate::ProcessNextURI()
"ProcessNextURI should only be called from the DOWNLOADING state");
if (mCurrentItem >= static_cast<PRInt32>(mItems.Length())) {
return Finish();
if (mPartialUpdate) {
return Finish();
} else {
// Verify that the manifest wasn't changed during the
// update, to prevent capturing a cache while the server
// is being updated. The check will call
// ManifestCheckCompleted() when it's done.
nsRefPtr<nsManifestCheck> manifestCheck =
new nsManifestCheck(this, mManifestURI, mDocumentURI);
if (NS_FAILED(manifestCheck->Begin())) {
mSucceeded = PR_FALSE;
NotifyError();
return Finish();
}
return NS_OK;
}
}
#if defined(PR_LOGGING)

View File

@ -138,6 +138,9 @@ public:
{ return (mParserState != PARSE_INIT && mParserState != PARSE_ERROR); }
PRBool NeedsUpdate() { return mParserState != PARSE_INIT && mNeedsUpdate; }
void GetManifestHash(nsCString &aManifestHash)
{ aManifestHash = mManifestHashValue; }
private:
static NS_METHOD ReadManifest(nsIInputStream *aInputStream,
void *aClosure,
@ -196,6 +199,7 @@ private:
// manifest hash data
nsCOMPtr<nsICryptoHash> mManifestHash;
PRBool mManifestHashInitialized;
nsCString mManifestHashValue;
nsCString mOldManifestHashValue;
};
@ -216,6 +220,8 @@ public:
nsresult Cancel();
void LoadCompleted();
void ManifestCheckCompleted(nsresult aStatus,
const nsCString &aManifestHash);
void AddDocument(nsIDOMDocument *aDocument) {
mDocuments.AppendObject(aDocument);
@ -281,6 +287,10 @@ private:
/* Documents that requested this update */
nsCOMArray<nsIDOMDocument> mDocuments;
/* Reschedule count. When an update is rescheduled due to
* mismatched manifests, the reschedule count will be increased. */
PRUint32 mRescheduleCount;
};
class nsOfflineCacheUpdateService : public nsIOfflineCacheUpdateService