Skip to content

Commit

Permalink
Bug 1714065 - Allow matching partition key fields via OriginAttribute…
Browse files Browse the repository at this point in the history
…sPattern. r=timhuang,ckerschb

Differential Revision: https://phabricator.services.mozilla.com/D116606
  • Loading branch information
Trikolon committed Jun 24, 2021
1 parent a52cda0 commit 19963a7
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 3 deletions.
71 changes: 71 additions & 0 deletions caps/OriginAttributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,75 @@ bool OriginAttributes::IsPrivateBrowsing(const nsACString& aOrigin) {
return !!attrs.mPrivateBrowsingId;
}

/* static */
bool OriginAttributes::ParsePartitionKey(const nsAString& aPartitionKey,
nsAString& outScheme,
nsAString& outBaseDomain,
int32_t& outPort) {
outScheme.Truncate();
outBaseDomain.Truncate();
outPort = -1;

// Partition keys have the format "(<scheme>,<baseDomain>,[port])". The port
// is optional. For example: "(https,example.com,8443)" or
// "(http,example.org)".
// When privacy.dynamic_firstparty.use_site = false, the partitionKey contains
// only the host, e.g. "example.com".
// See MakeTopLevelInfo for the partitionKey serialization code.

if (aPartitionKey.IsEmpty()) {
return true;
}

// PartitionKey contains only the host.
if (!StaticPrefs::privacy_dynamic_firstparty_use_site()) {
outBaseDomain = aPartitionKey;
return true;
}

// Smallest possible partitionKey is "(x,x)". Scheme and base domain are
// mandatory.
if (NS_WARN_IF(aPartitionKey.Length() < 5)) {
return false;
}

if (NS_WARN_IF(aPartitionKey.First() != '(' || aPartitionKey.Last() != ')')) {
return false;
}

// Remove outer brackets so we can string split.
nsAutoString str(Substring(aPartitionKey, 1, aPartitionKey.Length() - 2));

uint32_t fieldIndex = 0;
for (const nsAString& field : str.Split(',')) {
if (NS_WARN_IF(field.IsEmpty())) {
// There cannot be empty fields.
return false;
}

if (fieldIndex == 0) {
outScheme.Assign(field);
} else if (fieldIndex == 1) {
outBaseDomain.Assign(field);
} else if (fieldIndex == 2) {
// Parse the port which is represented in the partitionKey string as a
// decimal (base 10) number.
long port = strtol(NS_ConvertUTF16toUTF8(field).get(), nullptr, 10);
// Invalid port.
if (NS_WARN_IF(port == 0)) {
return false;
}
outPort = static_cast<int32_t>(port);
} else {
NS_WARNING("Invalid partitionKey. Too many tokens");
return false;
}

fieldIndex++;
}

// scheme and base domain are required.
return fieldIndex > 1;
}

} // namespace mozilla
66 changes: 63 additions & 3 deletions caps/OriginAttributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ class OriginAttributes : public dom::OriginAttributesDictionary {
// returns true if the originAttributes suffix has mPrivateBrowsingId value
// different than 0.
static bool IsPrivateBrowsing(const nsACString& aOrigin);

// Parse a partitionKey of the format "(<scheme>,<baseDomain>,[port])" into
// its components.
// Returns false if the partitionKey cannot be parsed because the format is
// invalid.
static bool ParsePartitionKey(const nsAString& aPartitionKey,
nsAString& outScheme, nsAString& outBaseDomain,
int32_t& outPort);
};

class OriginAttributesPattern : public dom::OriginAttributesPatternDictionary {
Expand Down Expand Up @@ -184,9 +192,42 @@ class OriginAttributesPattern : public dom::OriginAttributesPatternDictionary {
return false;
}

if (mPartitionKey.WasPassed() &&
mPartitionKey.Value() != aAttrs.mPartitionKey) {
return false;
// If both mPartitionKey and mPartitionKeyPattern are passed, mPartitionKey
// takes precedence.
if (mPartitionKey.WasPassed()) {
if (mPartitionKey.Value() != aAttrs.mPartitionKey) {
return false;
}
} else if (mPartitionKeyPattern.WasPassed()) {
auto& pkPattern = mPartitionKeyPattern.Value();

if (pkPattern.mScheme.WasPassed() || pkPattern.mBaseDomain.WasPassed() ||
pkPattern.mPort.WasPassed()) {
if (aAttrs.mPartitionKey.IsEmpty()) {
return false;
}

nsString scheme;
nsString baseDomain;
int32_t port;
bool success = OriginAttributes::ParsePartitionKey(
aAttrs.mPartitionKey, scheme, baseDomain, port);
if (!success) {
return false;
}

if (pkPattern.mScheme.WasPassed() &&
pkPattern.mScheme.Value() != scheme) {
return false;
}
if (pkPattern.mBaseDomain.WasPassed() &&
pkPattern.mBaseDomain.Value() != baseDomain) {
return false;
}
if (pkPattern.mPort.WasPassed() && pkPattern.mPort.Value() != port) {
return false;
}
}
}

return true;
Expand Down Expand Up @@ -227,6 +268,25 @@ class OriginAttributesPattern : public dom::OriginAttributesPatternDictionary {
return false;
}

if (mPartitionKeyPattern.WasPassed() &&
aOther.mPartitionKeyPattern.WasPassed()) {
auto& self = mPartitionKeyPattern.Value();
auto& other = aOther.mPartitionKeyPattern.Value();

if (self.mScheme.WasPassed() && other.mScheme.WasPassed() &&
self.mScheme.Value() != other.mScheme.Value()) {
return false;
}
if (self.mBaseDomain.WasPassed() && other.mBaseDomain.WasPassed() &&
self.mBaseDomain.Value() != other.mBaseDomain.Value()) {
return false;
}
if (self.mPort.WasPassed() && other.mPort.WasPassed() &&
self.mPort.Value() != other.mPort.Value()) {
return false;
}
}

return true;
}
};
Expand Down
159 changes: 159 additions & 0 deletions caps/tests/unit/test_oa_partitionKey_pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

/**
* Tests origin attributes partitionKey pattern matching.
*/

"use strict";

function testMatch(oa, pattern, shouldMatch = true) {
let msg = `Origin attributes should ${
shouldMatch ? "match" : "not match"
} pattern.`;
msg += ` oa: ${JSON.stringify(oa)} - pattern: ${JSON.stringify(pattern)}`;
Assert.equal(
ChromeUtils.originAttributesMatchPattern(oa, pattern),
shouldMatch,
msg
);
}

function getPartitionKey(scheme, baseDomain, port) {
if (!scheme || !baseDomain) {
return "";
}
return `(${scheme},${baseDomain}${port ? `,${port}` : ``})`;
}

function getOAWithPartitionKey(scheme, baseDomain, port, oa = {}) {
oa.partitionKey = getPartitionKey(scheme, baseDomain, port);
return oa;
}

/**
* Tests that an OriginAttributesPattern which is empty or only has an empty
* partitionKeyPattern matches any partitionKey.
*/
add_task(async function test_empty_pattern_matches_any() {
let list = [
getOAWithPartitionKey("https", "example.com"),
getOAWithPartitionKey("http", "example.net", 8080),
getOAWithPartitionKey(),
];

for (let oa of list) {
testMatch(oa, {});
testMatch(oa, { partitionKeyPattern: {} });
}
});

/**
* Tests that if a partitionKeyPattern is passed, but the partitionKey is
* invalid, the pattern match will always fail.
*/
add_task(async function test_invalid_pk() {
let list = [
"()",
"(,,)",
"(https)",
"(https,,)",
"(example.com)",
"(http,example.com,invalid)",
"(http,example.com,8000,1000)",
].map(partitionKey => ({ partitionKey }));

for (let oa of list) {
testMatch(oa, {});
testMatch(oa, { partitionKeyPattern: {} });
testMatch(
oa,
{ partitionKeyPattern: { baseDomain: "example.com" } },
false
);
testMatch(oa, { partitionKeyPattern: { scheme: "https" } }, false);
}
});

/**
* Tests that if a pattern sets "partitionKey" it takes precedence over "partitionKeyPattern".
*/
add_task(async function test_string_overwrites_pattern() {
let oa = getOAWithPartitionKey("https", "example.com", 8080, {
userContextId: 2,
});

testMatch(oa, { partitionKey: oa.partitionKey });
testMatch(oa, {
partitionKey: oa.partitionKey,
partitionKeyPattern: { baseDomain: "example.com" },
});
testMatch(oa, {
partitionKey: oa.partitionKey,
partitionKeyPattern: { baseDomain: "example.net" },
});
testMatch(
oa,
{
partitionKey: getPartitionKey("https", "example.net"),
partitionKeyPattern: { scheme: "https", baseDomain: "example.com" },
},
false
);
});

/**
* Tests that we can match parts of a partitionKey by setting
* partitionKeyPattern.
*/
add_task(async function test_pattern() {
let a = getOAWithPartitionKey("https", "example.com", 8080, {
userContextId: 2,
});
let b = getOAWithPartitionKey("https", "example.com", undefined, {
privateBrowsingId: 1,
});

for (let oa of [a, b]) {
// Match
testMatch(oa, { partitionKeyPattern: { scheme: "https" } });
testMatch(oa, {
partitionKeyPattern: { scheme: "https", baseDomain: "example.com" },
});
testMatch(
oa,
{
partitionKeyPattern: {
scheme: "https",
baseDomain: "example.com",
port: 8080,
},
},
oa == a
);
testMatch(oa, {
partitionKeyPattern: { baseDomain: "example.com" },
});
testMatch(
oa,
{
partitionKeyPattern: { port: 8080 },
},
oa == a
);

// Mismatch
testMatch(oa, { partitionKeyPattern: { scheme: "http" } }, false);
testMatch(
oa,
{ partitionKeyPattern: { baseDomain: "example.net" } },
false
);
testMatch(oa, { partitionKeyPattern: { port: 8443 } }, false);
testMatch(
oa,
{ partitionKeyPattern: { scheme: "https", baseDomain: "example.net" } },
false
);
}
});
1 change: 1 addition & 0 deletions caps/tests/unit/xpcshell.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ head =
[test_origin.js]
[test_uri_escaping.js]
[test_ipv6_host_literal.js]
[test_oa_partitionKey_pattern.js]
[test_site_origin.js]
8 changes: 8 additions & 0 deletions dom/chrome-webidl/ChromeUtils.webidl
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,15 @@ dictionary OriginAttributesPatternDictionary {
unsigned long privateBrowsingId;
DOMString firstPartyDomain;
DOMString geckoViewSessionContextId;
// partitionKey takes precedence over partitionKeyPattern.
DOMString partitionKey;
PartitionKeyPatternDictionary partitionKeyPattern;
};

dictionary PartitionKeyPatternDictionary {
DOMString scheme;
DOMString baseDomain;
long port;
};

dictionary CompileScriptOptionsDictionary {
Expand Down

0 comments on commit 19963a7

Please sign in to comment.