Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "NoVarySearchUtils.h"
#include <algorithm>
#include "mozilla/net/SFV.h"
#include "nsIURI.h"
#include "nsURLHelper.h"
namespace mozilla::net {
/**
* Extracts string values from an SFV InnerListResult into aOut, skipping
* non-string items. Used to collect parameter names from the "params" and
* "except" fields of a No-Vary-Search header.
*
* @param aList SFV InnerListResult to read items from.
* @param aOut [out] Receives the extracted string values.
*/
static void CollectInnerListStrings(const SFV::InnerListResult& aList,
nsTArray<nsCString>& aOut) {
for (size_t i = 0; i < aList.Length(); i++) {
nsAutoCString val;
if (NS_SUCCEEDED(aList.GetItemAt(i).GetValue<SFV::SFVString>(val))) {
aOut.AppendElement(val);
}
}
}
NoVarySearchData ParseNoVarySearchHeader(const nsACString& aHeader) {
NoVarySearchData data;
if (aHeader.IsEmpty()) {
return data;
}
auto dict = SFV::ParseDict(aHeader);
if (!dict.IsValid()) {
return data; // spec §5.1: parse error → default config (ExactMatch)
}
// key-order: if present and true, query parameter order is insignificant.
bool keyOrder = false;
if (NS_SUCCEEDED(dict.GetItem<SFV::SFVBool>("key-order"_ns, keyOrder))) {
data.varyOnKeyOrder = !keyOrder;
}
// "params" may be a boolean item or an inner list of strings (§5.1 steps
// 5-6):
// params=?1 → IgnoreAll (or Allowlist when except is present)
// params=(...) → Blocklist
// params=?1 + except → Allowlist
// params=(...) + except → invalid → ExactMatch
// except without params → invalid → ExactMatch
bool paramsBool = false;
bool paramsIsBool =
NS_SUCCEEDED(dict.GetItem<SFV::SFVBool>("params"_ns, paramsBool));
auto paramsInnerList = dict.GetInnerList("params"_ns);
auto exceptInnerList = dict.GetInnerList("except"_ns);
bool hasExcept = exceptInnerList.IsValid();
if (paramsIsBool && paramsBool) {
if (hasExcept) {
// params=?1 + except=(...) → Allowlist
data.paramsRule = NoVarySearchData::ParamsRule::Allowlist;
CollectInnerListStrings(exceptInnerList, data.paramNames);
} else {
data.paramsRule = NoVarySearchData::ParamsRule::IgnoreAll;
}
} else if (paramsInnerList.IsValid()) {
if (hasExcept) {
return NoVarySearchData{}; // params=(...) + except → invalid →
// ExactMatch
}
data.paramsRule = NoVarySearchData::ParamsRule::Blocklist;
CollectInnerListStrings(paramsInnerList, data.paramNames);
} else if (hasExcept) {
return NoVarySearchData{}; // except without valid params → invalid →
// ExactMatch
}
return data;
}
using Param = std::pair<nsCString, nsCString>;
/**
* Parses an application/x-www-form-urlencoded query string into an ordered
* list of (name, value) pairs.
*
* @param aQuery Query string without the leading '?'.
* @return Ordered list of (name, value) pairs.
*/
static nsTArray<Param> ParseQueryParams(const nsACString& aQuery) {
nsTArray<Param> params;
URLParams::Parse(
aQuery, true, [&params](nsCString&& name, nsCString&& value) {
params.AppendElement(Param{std::move(name), std::move(value)});
return true;
});
return params;
}
/**
* Filters a list of query (name, value) pairs according to the No-Vary-Search
* variation config, removing parameters that should not affect URL equivalence.
*
* @param aParams [in/out] Parameter list to filter in place.
* @param aData Parsed URL variation config controlling which params to keep.
*/
static void FilterParams(nsTArray<Param>& aParams,
const NoVarySearchData& aData) {
switch (aData.paramsRule) {
case NoVarySearchData::ParamsRule::IgnoreAll:
aParams.Clear();
break;
case NoVarySearchData::ParamsRule::Blocklist:
aParams.RemoveElementsBy(
[&](const Param& p) { return aData.paramNames.Contains(p.first); });
break;
case NoVarySearchData::ParamsRule::Allowlist:
aParams.RemoveElementsBy(
[&](const Param& p) { return !aData.paramNames.Contains(p.first); });
break;
case NoVarySearchData::ParamsRule::ExactMatch:
break;
}
}
bool URLsAreEquivalentModuloVariationConfig(nsIURI* aURIA, nsIURI* aURIB,
const NoVarySearchData& aData) {
// Step 1: scheme, host, and port must match exactly.
nsAutoCString prePathA, prePathB;
aURIA->GetPrePath(prePathA);
aURIB->GetPrePath(prePathB);
if (!prePathA.Equals(prePathB)) {
return false;
}
// Path must also match exactly — NVS only varies query parameters.
nsAutoCString filePathA, filePathB;
aURIA->GetFilePath(filePathA);
aURIB->GetFilePath(filePathB);
if (!filePathA.Equals(filePathB)) {
return false;
}
// Fast path: no param filtering and order matters → compare query strings
// directly.
if (aData.paramsRule == NoVarySearchData::ParamsRule::ExactMatch &&
aData.varyOnKeyOrder) {
nsAutoCString queryA, queryB;
aURIA->GetQuery(queryA);
aURIB->GetQuery(queryB);
return queryA.Equals(queryB);
}
// Steps 3-5: parse query strings into (name, value) pairs.
nsAutoCString queryA, queryB;
aURIA->GetQuery(queryA);
aURIB->GetQuery(queryB);
nsTArray<Param> paramsA = ParseQueryParams(queryA);
nsTArray<Param> paramsB = ParseQueryParams(queryB);
// Steps 6-7: remove parameters that should not affect equivalence.
FilterParams(paramsA, aData);
FilterParams(paramsB, aData);
// Step 8: if key-order=?1, sort both lists by parameter name so that
// reordered parameters compare as equal.
if (!aData.varyOnKeyOrder) {
auto cmp = [](const Param& a, const Param& b) { return a.first < b.first; };
std::stable_sort(paramsA.begin(), paramsA.end(), cmp);
std::stable_sort(paramsB.begin(), paramsB.end(), cmp);
}
// Steps 9-11: compare filtered (and optionally sorted) parameter lists.
if (paramsA.Length() != paramsB.Length()) {
return false;
}
for (size_t i = 0; i < paramsA.Length(); ++i) {
if (paramsA[i].first != paramsB[i].first ||
paramsA[i].second != paramsB[i].second) {
return false;
}
}
return true;
}
nsresult ExtractNoVarySearchBasePath(nsIURI* aURI, nsACString& aBasePath) {
MOZ_ASSERT(aURI, "aURI must not be null");
if (!aURI) {
return NS_ERROR_INVALID_ARG;
}
nsAutoCString prePath, filePath;
MOZ_TRY(aURI->GetPrePath(prePath));
MOZ_TRY(aURI->GetFilePath(filePath));
aBasePath.Assign(prePath);
aBasePath.Append(filePath);
return NS_OK;
}
} // namespace mozilla::net