Bug 1390465: Load all subtitles in RFP. r=tjr

This patch targets non-js mode. By fetching everything, the website can't tell what language is being used.

Trying to protect js enabled mode would be impossible as websites can just use a custom player and record usage as they wish.

Differential Revision: https://phabricator.services.mozilla.com/D223636
This commit is contained in:
Fatih 2024-10-18 12:13:17 +00:00
parent ee2988bdf0
commit 52c07c5f9d
7 changed files with 240 additions and 7 deletions

View File

@ -93,7 +93,8 @@ class WindowDestroyObserver final : public nsIObserver {
NS_ENSURE_SUCCESS(rv, rv);
if (innerID == mInnerID) {
if (mTrackElement) {
mTrackElement->CancelChannelAndListener();
// Window is being destroyed, cancel without checking for RFP.
mTrackElement->CancelChannelAndListener(false);
}
UnRegisterWindowDestroyObserver();
}
@ -128,7 +129,8 @@ HTMLTrackElement::~HTMLTrackElement() {
if (mWindowDestroyObserver) {
mWindowDestroyObserver->UnRegisterWindowDestroyObserver();
}
CancelChannelAndListener();
// Track element is being destroyed, cancel without checking for RFP.
CancelChannelAndListener(false);
}
NS_IMPL_ELEMENT_CLONE(HTMLTrackElement)
@ -250,11 +252,18 @@ void HTMLTrackElement::MaybeDispatchLoadResource() {
// step2, if the text track's text track mode is not set to one of hidden or
// showing, then return.
if (mTrack->Mode() == TextTrackMode::Disabled) {
bool resistFingerprinting = ShouldResistFingerprinting(RFPTarget::WebVTT);
if (mTrack->Mode() == TextTrackMode::Disabled && !resistFingerprinting) {
LOG("Do not load resource for disable track");
return;
}
// We need to do this check in order to prevent
// HonorUserPreferencesForTrackSelection from loading the text track twice.
if (resistFingerprinting && ReadyState() == TextTrackReadyState::Loading) {
return;
}
// step3, if the text track's track element does not have a media element as a
// parent, return.
if (!mMediaParent) {
@ -294,7 +303,8 @@ void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) {
NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv));
LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get());
CancelChannelAndListener();
// Prevent canceling the channel and listener if RFP is enabled.
CancelChannelAndListener(true);
// According to
// https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks
@ -470,7 +480,11 @@ void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) {
Cancelable::eNo);
}
void HTMLTrackElement::CancelChannelAndListener() {
void HTMLTrackElement::CancelChannelAndListener(bool aCheckRFP) {
if (aCheckRFP && ShouldResistFingerprinting(RFPTarget::WebVTT)) {
return;
}
if (mChannel) {
mChannel->CancelWithReason(NS_BINDING_ABORTED,
"HTMLTrackElement::CancelChannelAndListener"_ns);
@ -484,6 +498,15 @@ void HTMLTrackElement::CancelChannelAndListener() {
}
}
bool HTMLTrackElement::ShouldResistFingerprinting(RFPTarget aRfpTarget) {
Document* doc = OwnerDoc();
if (!doc) {
return nsContentUtils::ShouldResistFingerprinting("Null document",
aRfpTarget);
}
return doc->ShouldResistFingerprinting(aRfpTarget);
}
void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
const nsAttrValue* aValue,
const nsAttrValue* aOldValue,

View File

@ -98,7 +98,9 @@ class HTMLTrackElement final : public nsGenericHTMLElement {
void DispatchTrustedEvent(const nsAString& aName);
void DispatchTestEvent(const nsAString& aName);
void CancelChannelAndListener();
void CancelChannelAndListener(bool aCheckRFP);
bool ShouldResistFingerprinting(RFPTarget aRfpTarget);
// Only load resource for the non-disabled track with media parent.
void MaybeDispatchLoadResource();

View File

@ -114,7 +114,8 @@ WebVTTListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
mElement->SetReadyState(TextTrackReadyState::Loaded);
}
mElement->CancelChannelAndListener();
// Prevent canceling the channel and listener if RFP is enabled.
mElement->CancelChannelAndListener(true);
return aStatus;
}

View File

@ -0,0 +1,124 @@
// This file is a simple "server" for test_webvtt_resistfingerprinting.html
// You can request a VTT file by setting the `request` parameter to `vtt` and
// providing an `id` parameter. Id parameter is used to track how many times
// a specific test has requested the file. Please don't use the same id for
// multiple tests.
// You can also request the number of times a specific test has requested the
// file by setting the `request` parameter to `count` and providing an `id`
// parameter.
const vtt = `WEBVTT
REGION
id:testOne lines:2 width:30%
REGION
id:testTwo lines:4 width:20%
1
00:00.500 --> 00:00.700 region:testOne
This
2
00:01.200 --> 00:02.400 region:testTwo
Is
2.5
00:02.000 --> 00:03.500 region:testOne
(Over here?!)
3
00:02.710 --> 00:02.910
A
4
00:03.217 --> 00:03.989
Test
5
00:03.217 --> 00:03.989
And more!
`;
// stolen from server-stream-download.sjs# and they stole it from file_blocked_script.sjs
function setGlobalState(data, key) {
const x = {
data,
QueryInterface(_iid) {
return this;
},
};
x.wrappedJSObject = x;
setObjectState(key, x);
}
function getGlobalState(key) {
let data;
getObjectState(key, function (x) {
data = x && x.wrappedJSObject.data;
});
return data;
}
const requestCounter = (() => {
const keyPrefix = "vtt-request-counter-";
return {
recordRequest(id) {
const key = keyPrefix + id;
const count = getGlobalState(key) || 0;
setGlobalState(count + 1, key);
},
getRequestCount(id) {
const key = keyPrefix + id;
return getGlobalState(key) || 0;
},
};
})();
// We need this for test-verify jobs. It runs the test
// multiple times and we need to know how many times
// the test has been run because global state is not
// reset between runs.
const iterationCounter = (() => {
const keyPrefix = "vtt-request-iteration-counter-";
return {
recordIteration() {
const count = getGlobalState(keyPrefix) || 0;
setGlobalState(count + 1, keyPrefix);
},
getIterationCount() {
return getGlobalState(keyPrefix) || 0;
},
};
})();
function handleRequest(aRequest, aResponse) {
aResponse.setHeader("Access-Control-Allow-Origin", "*", false);
const params = aRequest.queryString
.split("&")
.map(command => command.split("="))
.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {});
if (params.request === "vtt") {
requestCounter.recordRequest(params.id);
aResponse.setStatusLine(aRequest.httpVersion, 200);
aResponse.setHeader("Content-Type", "text/vtt", false);
aResponse.write(vtt);
} else if (params.request === "count") {
aResponse.setStatusLine(aRequest.httpVersion, 200);
aResponse.write(requestCounter.getRequestCount(params.id));
} else if (params.request === "newIteration") {
iterationCounter.recordIteration();
aResponse.setStatusLine(aRequest.httpVersion, 200);
aResponse.write(iterationCounter.getIterationCount());
} else {
aResponse.setStatusLine(aRequest.httpVersion, 400);
aResponse.write(aRequest.queryString);
}
}

View File

@ -72,6 +72,12 @@ skip-if = ["true"]
["test_webvtt_positionalign.html"]
["test_webvtt_resistfingerprinting.html"]
support-files = [
"basicvtt-server.sjs",
]
run-sequentially = "An extension having the same id is installed/uninstalled in different tests"
["test_webvtt_seeking.html"]
["test_webvtt_update_display_after_adding_or_removing_cue.html"]

View File

@ -0,0 +1,76 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf-8'>
<title>WebVTT: ResistFingerprinting</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<div id="content">
</div>
<template id="video">
<video width="600" height="400" controls onloadeddata="handleOnLoadedData()">
<source src="gizmo.mp4" type="video/mp4">
<track label="VTT1" kind="subtitles" srclang="en" src="basicvtt-server.sjs?request=vtt&id=test_webvtt_resistfingerprinting.html" />
<track label="VTT2" kind="subtitles" srclang="es" src="basicvtt-server.sjs?request=vtt&id=test_webvtt_resistfingerprinting.html" />
<track label="VTT3" kind="subtitles" srclang="tr" src="basicvtt-server.sjs?request=vtt&id=test_webvtt_resistfingerprinting.html" default />
</video>
</template>
<script class="testbody" type="text/javascript">
SimpleTest.waitForExplicitFinish();
SimpleTest.requestFlakyTimeout("Need to wait for all VTT files to be loaded");
let iteration = "0";
async function startTest() {
await SpecialPowers.pushPrefEnv({
"set": [
["privacy.resistFingerprinting", true]
]
});
iteration = await fetch("basicvtt-server.sjs?request=newIteration")
.then(response => response.text());
const video = document.getElementById("video").content.cloneNode(true);
video.querySelectorAll("track").forEach(track => {
track.src = track.src + iteration;
});
document.getElementById("content").appendChild(video);
}
async function handleOnLoadedData() {
// Wait for 5 minutes at most
for (let i = 0; i < 5 * 60; i++) {
const shouldEnd = await fetch("basicvtt-server.sjs?request=count&id=test_webvtt_resistfingerprinting.html" + iteration)
.then(response => response.text())
.then(text => {
info("Number of VTT files loaded: " + text);
const num = +text;
if (num === 3) {
ok(true, "All VTT files are loaded");
return true
}
if (num > 3) {
ok(false, "Too many VTT files are loaded");
return true;
}
return false;
}).catch(error => {
ok(false, "Failed to fetch count: " + error);
return true;
});
if (shouldEnd) {
break;
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
await SpecialPowers.popPrefEnv();
SimpleTest.finish();
}
onload = startTest;
</script>
</body>
</html>

View File

@ -93,6 +93,7 @@ ITEM_VALUE(SiteSpecificZoom, 1llu << 60)
// Are font visibility restrictions applied when resolving a CSS <generic-family>?
// (This may block the fonts selected in Preferences from actually being used.)
ITEM_VALUE(FontVisibilityRestrictGenerics, 1llu << 61)
ITEM_VALUE(WebVTT, 1llu << 62)
// !!! Don't forget to update kDefaultFingerprintingProtections in nsRFPService.cpp
// if necessary.