tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 61b10d9023a9c14c3f6591ec9744dbde06d4ba6e
parent de8a7623b8b1b82ddac5f71066703400ca371f73
Author: Randell Jesup <rjesup@mozilla.com>
Date:   Wed,  1 Oct 2025 18:46:03 +0000

Bug 1917965: Update cache listener changes to ensure data is cached in compressed form r=necko-reviewers,kershaw

The changes to the listener to support saving to cache didn't
guarantee that compressed data would be stored in the cache in compressed
form.  Also the Content-Encoding values stored in the cache metadata could
be incorrect, leading to subtle CI failures.

Differential Revision: https://phabricator.services.mozilla.com/D264526

Diffstat:
Mnetwerk/protocol/http/nsHttpChannel.cpp | 314++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mnetwerk/protocol/http/nsHttpChannel.h | 7+++++++
2 files changed, 189 insertions(+), 132 deletions(-)

diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp @@ -3461,6 +3461,7 @@ void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval, ReportHttpResponseVersion(mResponseHead->Version()); } +// Only used for redirects (3XX responses) nsresult nsHttpChannel::ContinueProcessResponse4(nsresult rv) { bool doNotRender = DoNotRender3xxBody(rv); @@ -3546,20 +3547,48 @@ nsresult nsHttpChannel::ContinueProcessNormal(nsresult rv) { if (NS_FAILED(rv)) CloseCacheEntry(true); } - // We need to do this before CallonStartRequest, since this can modify - // the Content-Encoding to remove dcb/dcz (and perhaps others), and - // CallOnStartRequest() sends this to the content process. + // We may need to install the cache listener before CallonStartRequest, + // since InstallCacheListener can modify the Content-Encoding to remove + // dcb/dcz (and perhaps others), and CallOnStartRequest() sends the + // Content-Encoding to the content process. If this doesn't install a + // listener (because this isn't a dictionary or dictionary-compressed), + // call it after CallOnStartRequest so that we save the compressed data + // in the cache, and run the decompressor in the content process. + bool isDictionaryCompressed = false; + nsAutoCString contentEncoding; + Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + // Note: doesn't handle dcb, gzip or gzip, dcb (etc) + if (contentEncoding.Equals("dcb") || contentEncoding.Equals("dcz")) { + isDictionaryCompressed = true; + } - // Install cache listener if we still have a cache entry open if (mCacheEntry && !LoadCacheEntryIsReadOnly()) { - rv = InstallCacheListener(); - if (NS_FAILED(rv)) return rv; + // XXX We may want to consider recompressing any dcb/dcz files to save space + // and improve hitrate. Downside is CPU use, complexity and perhaps delay, + // maybe. + nsAutoCString dictionary; + if (StaticPrefs::network_http_dictionaries_enable() && IsHTTPS()) { + Unused << mResponseHead->GetHeader(nsHttp::Use_As_Dictionary, dictionary); + if (!dictionary.IsEmpty()) { + if (!ParseDictionary(mCacheEntry, mResponseHead.get(), true)) { + LOG_DICTIONARIES(("Failed to parse use-as-dictionary")); + } else { + MOZ_ASSERT(mDictSaving); + + // We need to record the hash as we save it + mCacheEntry->SetDictionary(mDictSaving); + } + } + } + + if (isDictionaryCompressed || mDictSaving) { + LOG(("Decompressing before saving into cache [channel=%p]", this)); + rv = DoInstallCacheListener(isDictionaryCompressed, &dictionary, 0); + } } else { - // We may still need to decompress in the parent if it's dcb or dcz - nsAutoCString contentEncoding; - Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, - contentEncoding); - if (contentEncoding.Equals("dcb") || contentEncoding.Equals("dcz")) { + if (isDictionaryCompressed) { + // We still need to decompress in the parent if it's dcb or dcz even if + // not saving to the cache LOG_DICTIONARIES( ("Removing Content-Encoding %s for %p", contentEncoding.get(), this)); nsCOMPtr<nsIStreamListener> listener; @@ -3581,7 +3610,7 @@ nsresult nsHttpChannel::ContinueProcessNormal(nsresult rv) { LOG_DICTIONARIES(("Didn't install decompressor without cache tee")); } } - } + } // else we'll call InstallCacheListener after CallOnStartRequest // Check that the server sent us what we were asking for if (LoadResuming()) { @@ -3624,6 +3653,15 @@ nsresult nsHttpChannel::ContinueProcessNormal(nsresult rv) { rv = CallOnStartRequest(); if (NS_FAILED(rv)) return rv; + // If we didn't install cache listeners to decompress above + // install the cache listener now (so they'll get compressed data) + if (!isDictionaryCompressed && !mDictSaving) { + // install cache listener if we still have a cache entry open + if (mCacheEntry && !LoadCacheEntryIsReadOnly()) { + rv = InstallCacheListener(); + if (NS_FAILED(rv)) return rv; + } + } return NS_OK; } @@ -5890,8 +5928,9 @@ nsresult nsHttpChannel::InitCacheEntry() { // mark this weakly framed until a response body is seen mCacheEntry->SetMetaDataElement("strongly-framed", "0"); - // We'll store the cache headers in InstallCacheListener, since it may modify - // them + rv = AddCacheEntryHeaders(mCacheEntry, false); + if (NS_FAILED(rv)) return rv; + StoreInitedCacheEntry(true); // Don't perform the check when writing (doesn't make sense) @@ -5951,6 +5990,36 @@ nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, rv = StoreAuthorizationMetaData(entry, requestHead); if (NS_FAILED(rv)) return rv; + rv = self->ProcessVaryCacheEntryHeaders(entry, nullptr); + if (NS_FAILED(rv)) return rv; + + // Store the received HTTP head with the cache entry as an element of + // the meta data. + nsAutoCString head; + responseHead->Flatten(head, true); + rv = entry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + head.Truncate(); + responseHead->FlattenNetworkOriginalHeaders(head); + rv = entry->SetMetaDataElement("original-response-headers", head.get()); + if (NS_FAILED(rv)) return rv; + + // Indicate we have successfully finished setting metadata on the cache entry. + rv = entry->MetaDataReady(); + + return rv; +} + +nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry, + bool aModified) { + return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(), + mSecurityInfo, aModified); +} + +nsresult nsHttpChannel::ProcessVaryCacheEntryHeaders(nsICacheEntry* entry, + const nsHttpAtom* aAtom) { + nsresult rv = NS_OK; + // Iterate over the headers listed in the Vary response header, and // store the value of the corresponding request header so we can verify // that it has not varied when we try to re-use the cached response at @@ -5964,68 +6033,66 @@ nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, // the check. { nsAutoCString buf, metaKey; - Unused << responseHead->GetHeader(nsHttp::Vary, buf); + Unused << mResponseHead->GetHeader(nsHttp::Vary, buf); constexpr auto prefix = "request-"_ns; for (const nsACString& token : nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) { LOG( - ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " + ("nsHttpChannel::ProcessVaryCacheEntryHeaders [this=%p] " "processing %s", - self, nsPromiseFlatCString(token).get())); + this, nsPromiseFlatCString(token).get())); if (!token.EqualsLiteral("*")) { nsHttpAtom atom = nsHttp::ResolveAtom(token); - nsAutoCString val; - nsAutoCString hash; - if (NS_SUCCEEDED(requestHead->GetHeader(atom, val))) { - // If cookie-header, store a hash of the value - if (atom == nsHttp::Cookie) { - LOG( - ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " - "cookie-value %s", - self, val.get())); - rv = Hash(val.get(), hash); - // If hash failed, store a string not very likely - // to be the result of subsequent hashes - if (NS_FAILED(rv)) { - val = "<hash failed>"_ns; - } else { - val = hash; + if (!aAtom || atom == *aAtom) { + nsAutoCString val; + nsAutoCString hash; + if (NS_SUCCEEDED(mRequestHead.GetHeader(atom, val))) { + // If cookie-header, store a hash of the value + if (atom == nsHttp::Cookie) { + LOG( + ("nsHttpChannel::ProcessVaryCacheEntryHeaders [this=%p] " + "cookie-value %s", + this, val.get())); + rv = Hash(val.get(), hash); + // If hash failed, store a string not very likely + // to be the result of subsequent hashes + if (NS_FAILED(rv)) { + val = "<hash failed>"_ns; + } else { + val = hash; + } + + LOG((" hashed to %s\n", val.get())); } - LOG((" hashed to %s\n", val.get())); + // build cache meta data key and set meta data element... + metaKey = prefix + token; + entry->SetMetaDataElement(metaKey.get(), val.get()); + } else { + LOG( + ("nsHttpChannel::ProcessVaryCacheEntryHeaders [this=%p] " + "clearing metadata for %s", + this, nsPromiseFlatCString(token).get())); + metaKey = prefix + token; + entry->SetMetaDataElement(metaKey.get(), nullptr); } - - // build cache meta data key and set meta data element... - metaKey = prefix + token; - entry->SetMetaDataElement(metaKey.get(), val.get()); - } else { - LOG( - ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " - "clearing metadata for %s", - self, nsPromiseFlatCString(token).get())); - metaKey = prefix + token; - entry->SetMetaDataElement(metaKey.get(), nullptr); } } } } + return rv; +} - // Store the received HTTP head with the cache entry as an element of - // the meta data. - nsAutoCString head; - responseHead->Flatten(head, true); - rv = entry->SetMetaDataElement("response-head", head.get()); - if (NS_FAILED(rv)) return rv; - head.Truncate(); - responseHead->FlattenNetworkOriginalHeaders(head); - rv = entry->SetMetaDataElement("original-response-headers", head.get()); - if (NS_FAILED(rv)) return rv; - - // Indicate we have successfully finished setting metadata on the cache entry. - rv = entry->MetaDataReady(); - +nsresult nsHttpChannel::ModifiedCacheEntryHeaders(nsICacheEntry* entry, + const nsHttpAtom& aAtom) { + nsresult rv = ProcessVaryCacheEntryHeaders(entry, &aAtom); + if (NS_SUCCEEDED(rv)) { + // Indicate we have successfully finished setting metadata on the cache + // entry. + rv = entry->MetaDataReady(); + } return rv; } @@ -6084,12 +6151,6 @@ bool nsHttpChannel::ParseDictionary(nsICacheEntry* aEntry, return true; // succeeded, no use-as-dictionary } -nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry, - bool aModified) { - return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(), - mSecurityInfo, aModified); -} - inline void GetAuthType(const char* challenge, nsCString& authType) { const char* p; @@ -6137,9 +6198,19 @@ nsresult nsHttpChannel::FinalizeCacheEntry() { return NS_OK; } -// Open an output stream to the cache entry and insert a listener tee into -// the chain of response listeners. nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { + return DoInstallCacheListener(false, nullptr, offset); +} + +// Open an output stream to the cache entry and insert a listener tee into +// the chain of response listeners, so the data will go the cache and the +// normal listener chain, which often will eventually include a +// decompressor. If the Content-Encoding is dcb or dcz, we'll include a +// decompressor *before* the tee, so the cache will see decompressed data +// (we can't decompress dcb/dcz when reading from the cache). +nsresult nsHttpChannel::DoInstallCacheListener(bool aIsDictionaryCompressed, + nsACString* aDictionary, + int64_t offset) { nsresult rv; LOG(("Preparing to write data into the cache [uri=%s]\n", mSpec.get())); @@ -6149,24 +6220,6 @@ nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { mRaceCacheWithNetwork); MOZ_ASSERT(mListener); - // XXX We may want to consider recompressing any dcb/dcz files to save space - // and improve hitrate. Downside is CPU use, complexity and perhaps delay, - // maybe. - nsAutoCString dictionary; - if (StaticPrefs::network_http_dictionaries_enable() && IsHTTPS()) { - Unused << mResponseHead->GetHeader(nsHttp::Use_As_Dictionary, dictionary); - if (!dictionary.IsEmpty()) { - // If this is being marked as a dictionary, add it to the list - if (!ParseDictionary(mCacheEntry, mResponseHead.get(), true)) { - LOG_DICTIONARIES(("Failed to parse use-as-dictionary")); - } else { - MOZ_ASSERT(mDictSaving); - } - } - - // We need to record the hash as we save it - mCacheEntry->SetDictionary(mDictSaving); - } LOG(("Trading cache input stream for output stream [channel=%p]", this)); // We must close the input stream first because cache entries do not @@ -6179,10 +6232,6 @@ nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { predictedSize -= offset; } - nsCOMPtr<nsIStreamListenerTee> tee = - do_CreateInstance(kStreamListenerTeeCID, &rv); - if (NS_FAILED(rv)) return rv; - nsCOMPtr<nsIOutputStream> out; rv = mCacheEntry->OpenOutputStream(offset, predictedSize, getter_AddRefs(out)); @@ -6218,6 +6267,10 @@ nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { if (NS_FAILED(rv)) return rv; #endif + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(kStreamListenerTeeCID, &rv); + if (NS_FAILED(rv)) return rv; + rv = tee->Init(mListener, out, nullptr); LOG(("nsHttpChannel::InstallCacheListener sync tee %p rv=%" PRIx32, tee.get(), static_cast<uint32_t>(rv))); @@ -6234,52 +6287,49 @@ nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { // before the content process gets to see it // XXX We could recompress this with e.g. gzip to save space and improve // hitrate, at the cost of some CPU. - nsAutoCString contentEncoding; - Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); - LOG_DICTIONARIES( - ("Content-Encoding for %p: %s", this, contentEncoding.get())); - if (!dictionary.IsEmpty() || contentEncoding.Equals("dcb") || - contentEncoding.Equals("dcz")) { - if (!contentEncoding.IsEmpty()) { + bool removedEncoding = false; + // Note: this doesn't handle cases like "dcb, gzip" or (worse?) "gzip, dcb". + // We could in theory handle them. + if (aDictionary || aIsDictionaryCompressed) { + nsCOMPtr<nsIStreamListener> listener; + // otherwise we won't convert in the parent process + SetApplyConversion(true); + rv = + DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); + if (NS_FAILED(rv)) { + return rv; + } + if (listener) { LOG_DICTIONARIES( - ("Removing Content-Encoding %s for %p", contentEncoding.get(), this)); - nsCOMPtr<nsIStreamListener> listener; - // otherwise we won't convert in the parent process - SetApplyConversion(true); - rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), - nullptr); - if (NS_FAILED(rv)) { - return rv; - } - if (listener) { - LOG_DICTIONARIES( - ("Installed nsHTTPCompressConv %p before tee", listener.get())); - mListener = listener; - mCompressListener = listener; - StoreHasAppliedConversion(true); + ("Installed nsHTTPCompressConv %p before tee", listener.get())); + mListener = listener; + mCompressListener = listener; + StoreHasAppliedConversion(true); + removedEncoding = true; + } else { + LOG_DICTIONARIES(("Didn't install decompressor before tee")); + } + if (removedEncoding) { + // Remove Available-Dictionary from Vary header if present if we + // decompressed and changed the content type. This avoids us refusing to + // match on a future load, for example if this dictionary was decoded from + // an earlier version using a dictionary (i.e. the update jquery to new + // version using the old version as a dictionary; no future load will use + // that old version). + // XXX It would be slightly more efficient to remove all at once + // instead of sequentially by passing an array of strings + RemoveFromVary(mResponseHead.get(), "available-dictionary"_ns); + RemoveFromVary(mResponseHead.get(), "accept-encoding"_ns); + } + } - } else { - LOG_DICTIONARIES(("Didn't install decompressor before tee")); - } + if (removedEncoding) { + // After decompressors, we modified Content-Encoding + rv = ModifiedCacheEntryHeaders(mCacheEntry, nsHttp::Content_Encoding); + if (NS_FAILED(rv)) { + mCacheEntry->AsyncDoom(nullptr); + return rv; } - // Remove Available-Dictionary from Vary header if present if we - // decompressed and changed the content type. This avoids us refusing to - // match on a future load, for example if this dictionary was decoded from - // an earlier version using a dictionary (i.e. the update jquery to new - // version using the old version as a dictionary; no future load will use - // that old version). - // XXX It would be slightly more efficient to remove all at once - // instead of sequentially by passing an array of strings - RemoveFromVary(mResponseHead.get(), "available-dictionary"_ns); - RemoveFromVary(mResponseHead.get(), "accept-encoding"_ns); - } - - // Must add these after adding possible decompressors, since that may modify - // Content-Encoding - rv = AddCacheEntryHeaders(mCacheEntry, true); - if (NS_FAILED(rv)) { - mCacheEntry->AsyncDoom(nullptr); - return rv; } return NS_OK; } diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h @@ -421,8 +421,15 @@ class nsHttpChannel final : public HttpBaseChannel, bool aModified); [[nodiscard]] nsresult AddCacheEntryHeaders(nsICacheEntry* entry, bool aModified); + [[nodiscard]] nsresult ProcessVaryCacheEntryHeaders(nsICacheEntry* entry, + const nsHttpAtom* aAtom); + [[nodiscard]] nsresult ModifiedCacheEntryHeaders(nsICacheEntry* entry, + const nsHttpAtom& aAtom); [[nodiscard]] nsresult FinalizeCacheEntry(); [[nodiscard]] nsresult InstallCacheListener(int64_t offset = 0); + [[nodiscard]] nsresult DoInstallCacheListener(bool aIsDictionaryCompressed, + nsACString* aDictionary, + int64_t offset = 0); void MaybeInvalidateCacheEntryForSubsequentGet(); void AsyncOnExamineCachedResponse();