tor-browser

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

commit 77792ab3e3180d04ce426dec9b6bb4f6ad30f2d2
parent e0b8468a2e4550756eae119830eba4e9fbb1d50d
Author: Randell Jesup <rjesup@mozilla.com>
Date:   Tue,  7 Oct 2025 17:55:53 +0000

Bug 1917982: Support use_as_dictionary response headers r=necko-reviewers,kershaw

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

Diffstat:
Mnetwerk/base/nsNetUtil.cpp | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/base/nsNetUtil.h | 7+++++++
Mnetwerk/cache2/CacheEntry.h | 5+++++
Mnetwerk/cache2/CacheFileIOManager.cpp | 1+
Mnetwerk/cache2/Dictionary.h | 2++
Mnetwerk/protocol/http/HttpBaseChannel.h | 4++++
Mnetwerk/protocol/http/nsHttpAtomList.h | 1+
Mnetwerk/protocol/http/nsHttpChannel.cpp | 39+++++++++++++++++++++++++++++++++++++++
Mnetwerk/protocol/http/nsHttpChannel.h | 2++
Mnetwerk/protocol/http/nsHttpHandler.cpp | 7++++++-
Anetwerk/test/gtest/TestUseAsDictionary.cpp | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnetwerk/test/gtest/moz.build | 1+
12 files changed, 416 insertions(+), 1 deletion(-)

diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp @@ -4044,6 +4044,128 @@ void WarnIgnoredPreload(const mozilla::dom::Document& aDoc, nsIURI& aURI) { "PreloadIgnoredInvalidAttr", params); } +bool NS_ParseUseAsDictionary(const nsACString& aValue, nsACString& aMatch, + nsACString& aMatchId, + nsTArray<nsCString>& aMatchDestItems, + nsACString& aType) { + // Note: match= is required + // Use-As-Dictionary = %s"match" / + // %il"match-dest" / + // %s"id" / + // %t"type" ; case-sensitive + + nsCOMPtr<nsISFVService> sfv = GetSFVService(); + + nsCOMPtr<nsISFVDictionary> parsedHeader; + nsresult rv; + if (NS_FAILED( + rv = sfv->ParseDictionary(aValue, getter_AddRefs(parsedHeader)))) { + return false; + } + + nsCOMPtr<nsISFVItemOrInnerList> match; + rv = parsedHeader->Get("match"_ns, getter_AddRefs(match)); + if (NS_FAILED(rv)) { + return false; // match is required, fail if not found + } + if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(match)) { + nsCOMPtr<nsISFVBareItem> value; + rv = listItem->GetValue(getter_AddRefs(value)); + if (NS_FAILED(rv)) { + return false; + } + if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) { + if (NS_FAILED(stringVal->GetValue(aMatch))) { + return false; + } + if (aMatch.IsEmpty()) { + return false; // match is required, fail if not found + } + } else { + return false; + } + } else { + return false; + } + + nsCOMPtr<nsISFVItemOrInnerList> matchdest; + rv = parsedHeader->Get("match-dest"_ns, getter_AddRefs(matchdest)); + if (NS_SUCCEEDED(rv)) { + if (nsCOMPtr<nsISFVInnerList> innerList = do_QueryInterface(matchdest)) { + // Extract the first entry of each inner list, which should contain the + // endpoint's URL string + nsTArray<RefPtr<nsISFVItem>> items; + if (NS_FAILED(innerList->GetItems(items))) { + return false; + } + // Don't check items.IsEmpty() because an empty list is valid + + for (auto& item : items) { + nsCOMPtr<nsISFVBareItem> value; + if (NS_FAILED(item->GetValue(getter_AddRefs(value)))) { + return false; + } + if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) { + nsAutoCString string; + if (NS_FAILED(stringVal->GetValue(string))) { + return false; + } + aMatchDestItems.AppendElement(string); + } else { + return false; // match-dest is an inner list of strings + } + } + } + } + + nsCOMPtr<nsISFVItemOrInnerList> matchid; + rv = parsedHeader->Get("id"_ns, getter_AddRefs(matchid)); + if (NS_SUCCEEDED(rv)) { + if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(matchid)) { + nsCOMPtr<nsISFVBareItem> value; + rv = listItem->GetValue(getter_AddRefs(value)); + if (NS_FAILED(rv)) { + return false; + } + if (nsCOMPtr<nsISFVString> stringVal = do_QueryInterface(value)) { + if (NS_FAILED(stringVal->GetValue(aMatchId))) { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + nsCOMPtr<nsISFVItemOrInnerList> type; + rv = parsedHeader->Get("type"_ns, getter_AddRefs(type)); + if (NS_SUCCEEDED(rv)) { + if (nsCOMPtr<nsISFVItem> listItem = do_QueryInterface(type)) { + nsCOMPtr<nsISFVBareItem> value; + rv = listItem->GetValue(getter_AddRefs(value)); + if (NS_FAILED(rv)) { + return false; + } + if (nsCOMPtr<nsISFVToken> tokenVal = do_QueryInterface(value)) { + if (NS_FAILED(tokenVal->GetValue(aType))) { + return false; + } + if (!aType.Equals("raw"_ns)) { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + return true; +} + nsresult HasRootDomain(const nsACString& aInput, const nsACString& aHost, bool* aResult) { if (NS_WARN_IF(!aResult)) { diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h @@ -1165,6 +1165,13 @@ bool CheckPreloadAttrs(const nsAttrValue& aAs, const nsAString& aType, mozilla::dom::Document* aDocument); void WarnIgnoredPreload(const mozilla::dom::Document&, nsIURI&); +// Implements parsing of Use-As-Dictionary headers for Compression Dictionary +// support. +bool NS_ParseUseAsDictionary(const nsACString& aValue, nsACString& aMatch, + nsACString& aMatchId, + nsTArray<nsCString>& aMatchDestItems, + nsACString& aType); + /** * Returns true if the |aInput| in is part of the root domain of |aHost|. * For example, if |aInput| is "www.mozilla.org", and we pass in diff --git a/netwerk/cache2/CacheEntry.h b/netwerk/cache2/CacheEntry.h @@ -24,6 +24,7 @@ #include "mozilla/Attributes.h" #include "mozilla/Mutex.h" #include "mozilla/TimeStamp.h" +#include "Dictionary.h" static inline uint32_t PRTimeToSeconds(PRTime t_usec) { return uint32_t(t_usec / PR_USEC_PER_SEC); @@ -130,6 +131,8 @@ class CacheEntry final : public nsIRunnable, TimeStamp const& LoadStart() const { return mLoadStart; } + void SetDictionary(DictionaryCacheEntry* aDict) { mDict = aDict; } + enum EPurge { PURGE_DATA_ONLY_DISK_BACKED, PURGE_WHOLE_ONLY_DISK_BACKED, @@ -427,6 +430,8 @@ class CacheEntry final : public nsIRunnable, mozilla::TimeStamp mLoadStart; uint32_t mUseCount{0}; + RefPtr<DictionaryCacheEntry> mDict; + const uint64_t mCacheEntryId; }; diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp @@ -3384,6 +3384,7 @@ nsresult CacheFileIOManager::EvictByContext( LOG(("CacheFileIOManager::EvictByContext() [loadContextInfo=%p]", aLoadContextInfo)); + // XXX evict dictionary data from memory cache nsresult rv; RefPtr<CacheFileIOManager> ioMan = gInstance; diff --git a/netwerk/cache2/Dictionary.h b/netwerk/cache2/Dictionary.h @@ -19,6 +19,8 @@ #include "nsString.h" #include "nsTArray.h" #include "mozilla/TimeStamp.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" class nsICacheStorage; class nsIIOService; diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h @@ -874,6 +874,10 @@ class HttpBaseChannel : public nsHashPropertyBag, nsIRequest::TRRMode mEffectiveTRRMode = nsIRequest::TRR_DEFAULT_MODE; TRRSkippedReason mTRRSkipReason = TRRSkippedReason::TRR_UNSET; + // Dictionary entry - retain while we're saving the data, and while we're + // fetching data possibly encoded with the entry + RefPtr<DictionaryCacheEntry> mDict; + public: void SetEarlyHints( nsTArray<mozilla::net::EarlyHintConnectArgs>&& aEarlyHints); diff --git a/netwerk/protocol/http/nsHttpAtomList.h b/netwerk/protocol/http/nsHttpAtomList.h @@ -104,6 +104,7 @@ HTTP_ATOM(Transfer_Encoding, "Transfer-Encoding") HTTP_ATOM(URI, "URI") HTTP_ATOM(Upgrade, "Upgrade") HTTP_ATOM(User_Agent, "User-Agent") +HTTP_ATOM(Use_As_Dictionary, "Use-As-Dictionary") HTTP_ATOM(Vary, "Vary") HTTP_ATOM(Version, "Version") HTTP_ATOM(WWW_Authenticate, "WWW-Authenticate") diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp @@ -113,6 +113,7 @@ #include "nsISocketProvider.h" #include "mozilla/extensions/StreamFilterParent.h" #include "mozilla/net/Predictor.h" +#include "mozilla/net/SFVService.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/NullPrincipal.h" #include "CacheControlParser.h" @@ -5899,12 +5900,50 @@ nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, rv = entry->SetMetaDataElement("original-response-headers", head.get()); if (NS_FAILED(rv)) return rv; + // If this is being marked as a dictionary, add it to the list + self->ParseDictionary(entry, responseHead); + // Indicate we have successfully finished setting metadata on the cache entry. rv = entry->MetaDataReady(); return rv; } +bool nsHttpChannel::ParseDictionary(nsICacheEntry* aEntry, + nsHttpResponseHead* aResponseHead) { + nsAutoCString val; + if (NS_SUCCEEDED(aResponseHead->GetHeader(nsHttp::Use_As_Dictionary, val))) { + nsAutoCStringN<128> matchVal; + nsAutoCStringN<64> matchIdVal; + nsTArray<nsCString> matchDestItems; + nsAutoCString typeVal; + + if (!NS_ParseUseAsDictionary(val, matchVal, matchIdVal, matchDestItems, + typeVal)) { + return false; + } + + nsCString key; + nsresult rv; + if (NS_FAILED(rv = aEntry->GetKey(key))) { + return false; + } + nsCString hash; + // Available now for use + RefPtr<DictionaryCache> dicts(DictionaryCache::GetInstance()); + LOG( + ("Adding DictionaryCache entry for %s: key %s, matchval %s, id=%s, " + "type=%s", + mURI->GetSpecOrDefault().get(), key.get(), matchVal.get(), + matchIdVal.get(), typeVal.get())); + dicts->AddEntry(mURI, key, matchVal, matchIdVal, Some(hash), + getter_AddRefs(mDict)); + mDict->InUse(); + return true; + } + return false; +} + nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry) { return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(), mSecurityInfo); diff --git a/netwerk/protocol/http/nsHttpChannel.h b/netwerk/protocol/http/nsHttpChannel.h @@ -412,6 +412,8 @@ class nsHttpChannel final : public HttpBaseChannel, void CloseCacheEntry(bool doomOnFailure); [[nodiscard]] nsresult InitCacheEntry(); void UpdateInhibitPersistentCachingFlag(); + bool ParseDictionary(nsICacheEntry* aEntry, + nsHttpResponseHead* aResponseHead); [[nodiscard]] nsresult AddCacheEntryHeaders(nsICacheEntry* entry); [[nodiscard]] nsresult FinalizeCacheEntry(); [[nodiscard]] nsresult InstallCacheListener(int64_t offset = 0); diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp @@ -670,8 +670,11 @@ nsresult nsHttpHandler::AddAcceptAndDictionaryHeaders( if (NS_FAILED(rv)) { return rv; } + LOG(("Setting Accept-Encoding: %s", + PromiseFlatCString(mDictionaryAcceptEncodings).get())); - LOG(("Found dictionary %s", PromiseFlatCString(dict->GetHash()).get())); + LOG(("Setting Available-Dictionary: %s", + PromiseFlatCString(dict->GetHash()).get())); rv = aRequest->SetHeader(nsHttp::Available_Dictionary, dict->GetHash(), false, nsHttpHeaderArray::eVarietyRequestOverride); if (NS_FAILED(rv)) { @@ -683,6 +686,8 @@ nsresult nsHttpHandler::AddAcceptAndDictionaryHeaders( if (NS_FAILED(rv)) { return rv; } + LOG(("Setting Dictionary-Id: %s", + PromiseFlatCString(dict->GetId()).get())); } // Need to retain access to the dictionary until the request completes. // Note that this includes if the dictionary we offered gets replaced diff --git a/netwerk/test/gtest/TestUseAsDictionary.cpp b/netwerk/test/gtest/TestUseAsDictionary.cpp @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <ostream> + +#include "gtest/gtest-param-test.h" +#include "gtest/gtest.h" + +#include "mozilla/gtest/MozAssertions.h" +#include "nsNetUtil.h" + +using namespace mozilla::net; + +struct TestData { + bool mResult; + const nsCString mHeader; + // output to match: + const nsCString mMatchVal; + const nsCString mMatchIdVal; + const nsCString mTypeVal; + // matchDestVal ends with ""_ns + const nsCString mMatchDestVal[5]; +}; + +TEST(TestUseAsDictionary, Match) +{ + // Note: we're not trying to test Structured Fields + // (https://datatracker.ietf.org/doc/html/rfc8941) here, but the data within + // it, so generally we aren't looking for format errors + const struct TestData gTestArray[] = { + {true, + "match=\"/app/*/main.js\""_ns, + "/app/*/main.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + {true, + "match=\"/app/*/main.js\", id=\"some_id\""_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + ""_ns, + {""_ns}}, + // match= is required + {false, "id=\"some_id\""_ns, ""_ns, "some_id"_ns, ""_ns, {""_ns}}, + {true, + "match=\"/app/*/main.js\", id=\"some_id\", type=raw"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + "raw"_ns, + {""_ns}}, + // only raw is supported for type + {false, + "match=\"/app/*/main.js\", id=\"some_id\", type=not_raw"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + "raw"_ns, + {""_ns}}, + {true, + "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\")"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + ""_ns, + {"style"_ns, ""_ns}}, + {true, + "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\"), type=raw"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + "raw"_ns, + {"style"_ns, ""_ns}}, + {true, + "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\" \"document\"), type=raw"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + "raw"_ns, + {"style"_ns, "document"_ns, ""_ns}}, + // adding the comma after style is a syntax error for structured fields + {false, + "match=\"/app/*/main.js\", id=\"some_id\", match-dest=(\"style\", \"document\"), type=raw"_ns, + "/app/*/main.js"_ns, + "some_id"_ns, + "raw"_ns, + {"style"_ns, "document"_ns, ""_ns}}, + // --- Additional tests for spec compliance and edge cases --- + // 1. Missing quotes around match value + {false, + "match=/app/*/main.js, id=\"id1\""_ns, + ""_ns, + "id1"_ns, + ""_ns, + {""_ns}}, + // 2. Extra unknown parameter + {true, + "match=\"/foo.js\", foo=bar"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + // 3. Whitespace variations + {true, + " match=\"/foo.js\" , id=\"id2\" "_ns, + "/foo.js"_ns, + "id2"_ns, + ""_ns, + {""_ns}}, + // 4. Empty match value + {false, "match=\"\""_ns, ""_ns, ""_ns, ""_ns, {""_ns}}, + // 5. Duplicate match parameter (should use the last) + {true, + "match=\"/foo.js\", match=\"/bar.js\""_ns, + "/bar.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + // 6. Duplicate id parameter (should use the last) + {true, + "match=\"/foo.js\", id=\"id1\", id=\"id2\""_ns, + "/foo.js"_ns, + "id2"_ns, + ""_ns, + {""_ns}}, + // 7. Parameter order: id before match + {true, + "id=\"id3\", match=\"/foo.js\""_ns, + "/foo.js"_ns, + "id3"_ns, + ""_ns, + {""_ns}}, + // 8. Non-raw type (should fail) + {false, + "match=\"/foo.js\", type=compressed"_ns, + "/foo.js"_ns, + ""_ns, + "raw"_ns, + {""_ns}}, + // 9. Empty header + {false, ""_ns, ""_ns, ""_ns, ""_ns, {""_ns}}, + // 10. match-dest with empty list + {true, + "match=\"/foo.js\", match-dest=()"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + // 11. match-dest with whitespace and multiple values + {true, + "match=\"/foo.js\", match-dest=( \"a\" \"b\" )"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {"a"_ns, "b"_ns, ""_ns}}, + // 12. match-dest with invalid value (missing quotes) + {false, + "match=\"/foo.js\", match-dest=(a)"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + // 13. match-dest with duplicate values + {true, + "match=\"/foo.js\", match-dest=(\"a\" \"a\")"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {"a"_ns, "a"_ns, ""_ns}}, + // 14. Case sensitivity: type=RAW (should fail, only 'raw' allowed) + {false, + "match=\"/foo.js\", type=RAW"_ns, + "/foo.js"_ns, + ""_ns, + "raw"_ns, + {""_ns}}, + + // Note: Structured Fields requires all input to be ASCII + + // 18. match-dest with trailing whitespace + {true, + "match=\"/foo.js\", match-dest=(\"a\" )"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {"a"_ns, ""_ns}}, + // 19. match-dest with only whitespace in list (should be empty) + {true, + "match=\"/foo.js\", match-dest=( )"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {""_ns}}, + // 20. match-dest with comma and whitespace (invalid) + {false, + "match=\"/foo.js\", match-dest=(\"a\", \"b\")"_ns, + "/foo.js"_ns, + ""_ns, + ""_ns, + {"a"_ns, "b"_ns, ""_ns}}, + }; + + for (auto& test : gTestArray) { + nsCString match, matchId, type; + nsTArray<nsCString> matchDest; + nsTArray<nsCString> matchDestVal; + + for (auto& dest : test.mMatchDestVal) { + if (dest.IsEmpty()) { + break; + } + matchDestVal.AppendElement(dest); + } + + fprintf(stderr, "Testing %s\n", test.mHeader.get()); + ASSERT_EQ( + NS_ParseUseAsDictionary(test.mHeader, match, matchId, matchDest, type), + test.mResult); + + if (test.mResult) { + ASSERT_EQ(match, test.mMatchVal); + ASSERT_EQ(matchId, test.mMatchIdVal); + ASSERT_EQ(matchDest.Length(), matchDestVal.Length()); + for (size_t i = 0; i < matchDest.Length(); i++) { + ASSERT_EQ(matchDest[i], matchDestVal[i]); + } + ASSERT_EQ(type, test.mTypeVal); + } + } +} diff --git a/netwerk/test/gtest/moz.build b/netwerk/test/gtest/moz.build @@ -41,6 +41,7 @@ UNIFIED_SOURCES += [ "TestURIMutator.cpp", "TestUriTemplate.cpp", "TestURLPatternGlue.cpp", + "TestUseAsDictionary.cpp", "TestWebTransportFlowControl.cpp", ]