Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extension: Re-enable SWF takeover using declarativeNetRequest #16694

Merged
merged 11 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion web/packages/extension/assets/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
"settings_autostart": {
"message": "Autoplay Flash content (click to unmute)"
},
"settings_swf_takeover": {
"message": "Play SWF files in browser instead of downloading"
},
"settings_log_level": {
"message": "Log level"
},
Expand Down Expand Up @@ -108,7 +111,7 @@
"message": "Permanently enable for this site"
},
"permissions_explanation": {
"message": "The Ruffle extension works best with the ability to access data on all websites. This allows it to run when any website first loads, therefore allowing it to convince websites that do Flash Player detection immediately to let the Flash content run. It also gives it the ability to load SWF files into its internal player page from other locations on the web. If you do not grant this permission, you will need to enable Ruffle manually on every website on which you wish to use it by clicking the icon for the extension and enabling it for that website."
"message": "The Ruffle extension works best with the ability to access data on all websites. This allows it to run when any website first loads, therefore allowing it to convince websites that do Flash Player detection immediately to let the Flash content run. It also gives it the ability to load SWF files into its internal player page from other locations on the web and allows it to automatically load direct SWF links into that page. If you do not grant this permission, you will need to enable Ruffle manually on every website on which you wish to use it by clicking the icon for the extension and enabling it for that website."
},
"swf_player_permissions": {
"message": "You must grant permission to this URL origin to load SWFs from it."
Expand Down
4 changes: 4 additions & 0 deletions web/packages/extension/assets/css/options.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
.hidden {
display: none;
}

.logo {
max-width: 224px;
}
Expand Down
4 changes: 4 additions & 0 deletions web/packages/extension/assets/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<input type="checkbox" id="autostart" />
<label for="autostart">Autoplay Flash content (click to unmute)</label>
</div>
<div class="option checkbox">
<input type="checkbox" id="swf_takeover" />
<label for="swf_takeover">Play SWF files in browser instead of downloading</label>
</div>
<div id="advanced-options">Advanced Options</div>
<div class="option checkbox">
<input type="checkbox" id="ignore_optout" />
Expand Down
4 changes: 4 additions & 0 deletions web/packages/extension/assets/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
<input type="checkbox" id="autostart" />
<label for="autostart">Autoplay Flash content (click to unmute)</label>
</div>
<div class="option checkbox">
<input type="checkbox" id="swf_takeover" />
<label for="swf_takeover">Play SWF files in browser instead of downloading</label>
</div>
</div>
<div class="buttons-container">
<button class="hidden" id="permissions-button">Permanently enable for this site</button>
Expand Down
2 changes: 1 addition & 1 deletion web/packages/extension/manifest.json5
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"page": "options.html",
"open_in_tab": true,
},
"host_permissions": ["<all_urls>"], // To allow script injecting + the internal player to bypass CORS
"host_permissions": ["<all_urls>"], // To allow script injecting + the internal player to bypass CORS + SWF takeover
"permissions": [
"storage",
"scripting",
Expand Down
165 changes: 165 additions & 0 deletions web/packages/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,164 @@ async function contentScriptRegistered() {
return matchingScripts?.length > 0;
}

// Copied from https://github.com/w3c/webextensions/issues/638#issuecomment-2181124486
async function isHeaderConditionSupported() {
danielhjacobs marked this conversation as resolved.
Show resolved Hide resolved
let needCleanup = false;
const ruleId = 4;
try {
// Throws synchronously if not supported.
await utils.declarativeNetRequest.updateDynamicRules({
addRules: [
{
id: ruleId,
condition: { responseHeaders: [{ header: "whatever" }] },
action: {
type:
chrome.declarativeNetRequest.RuleActionType
danielhjacobs marked this conversation as resolved.
Show resolved Hide resolved
?.BLOCK ?? "block",
},
},
],
});
needCleanup = true;
} catch {
return false; // responseHeaders condition not supported.
}
// Chrome may recognize the properties but have the implementation behind a flag.
// When the implementation is disabled, validation is skipped too.
try {
await utils.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId],
addRules: [
{
id: ruleId,
condition: { responseHeaders: [] },
action: {
type:
chrome.declarativeNetRequest.RuleActionType
?.BLOCK ?? "block",
},
},
],
});
needCleanup = true;
return false; // Validation skipped = feature disabled.
} catch {
return true; // Validation worked = feature enabled.
} finally {
if (needCleanup) {
await utils.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId],
});
}
}
}

async function enableSWFTakeover() {
// Checks if the responseHeaders condition is supported and not behind a disabled flag.
if (utils.declarativeNetRequest && (await isHeaderConditionSupported())) {
const { ruffleEnable } = await utils.getOptions();
if (ruffleEnable) {
const playerPage = utils.runtime.getURL("/player.html");
const rules = [
{
id: 1,
action: {
type:
chrome.declarativeNetRequest.RuleActionType
?.REDIRECT ?? "redirect",
redirect: { regexSubstitution: playerPage + "#\\0" },
},
condition: {
regexFilter: ".*",
responseHeaders: [
{
header: "content-type",
values: [
"application/x-shockwave-flash",
"application/futuresplash",
"application/x-shockwave-flash2-preview",
"application/vnd.adobe.flash.movie",
],
},
],
resourceTypes: [
chrome.declarativeNetRequest.ResourceType
?.MAIN_FRAME ?? "main_frame",
],
},
},
{
id: 2,
action: {
type:
chrome.declarativeNetRequest.RuleActionType
?.REDIRECT ?? "redirect",
redirect: { regexSubstitution: playerPage + "#\\0" },
},
condition: {
regexFilter:
"^.*:\\/\\/.*\\/.*\\.s(?:wf|pl)(\\?.*|#.*|)$",
responseHeaders: [
{
header: "content-type",
values: [
"application/octet-stream",
"application/binary-stream",
"",
],
},
],
resourceTypes: [
chrome.declarativeNetRequest.ResourceType
?.MAIN_FRAME ?? "main_frame",
],
},
},
{
id: 3,
action: {
type:
chrome.declarativeNetRequest.RuleActionType
?.REDIRECT ?? "redirect",
redirect: { regexSubstitution: playerPage + "#\\0" },
},
condition: {
regexFilter:
"^.*:\\/\\/.*\\/.*\\.s(?:wf|pl)(\\?.*|#.*|)$",
excludedResponseHeaders: [{ header: "content-type" }],
resourceTypes: [
chrome.declarativeNetRequest.ResourceType
?.MAIN_FRAME ?? "main_frame",
],
},
},
];
await utils.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1, 2, 3],
addRules: rules,
});
}
} else {
utils.storage.sync.set({ responseHeadersUnsupported: true });
}
}

async function disableSWFTakeover() {
if (utils.declarativeNetRequest && (await isHeaderConditionSupported())) {
await utils.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1, 2, 3],
});
} else {
utils.storage.sync.set({ responseHeadersUnsupported: true });
}
}

async function enable() {
const { swfTakeover } = await utils.getOptions();
if (swfTakeover) {
await enableSWFTakeover();
}
if (
!utils.scripting ||
(utils.scripting.ExecutionWorld && !utils.scripting.ExecutionWorld.MAIN)
Expand Down Expand Up @@ -57,6 +214,7 @@ async function disable() {
ids: ["plugin-polyfill", "4399"],
});
}
await disableSWFTakeover();
}

async function onAdded(permissions: chrome.permissions.Permissions) {
Expand Down Expand Up @@ -106,6 +264,13 @@ utils.storage.onChanged.addListener(async (changes, namespace) => {
await disable();
}
}
if (namespace === "sync" && "swfTakeover" in changes) {
if (changes["swfTakeover"]!.newValue) {
await enableSWFTakeover();
} else {
await disableSWFTakeover();
}
}
});

async function handleInstalled(details: chrome.runtime.InstalledDetails) {
Expand Down
1 change: 1 addition & 0 deletions web/packages/extension/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Options extends BaseLoadOptions {
ignoreOptout: boolean;
autostart: boolean;
showReloadButton: boolean;
swfTakeover: boolean;
}

interface OptionElement<T> {
Expand Down
10 changes: 9 additions & 1 deletion web/packages/extension/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import * as utils from "./utils";
import { bindOptions, resetOptions } from "./common";
import { buildInfo } from "ruffle-core";

window.addEventListener("DOMContentLoaded", () => {
window.addEventListener("DOMContentLoaded", async () => {
const data = await utils.storage.sync.get({
responseHeadersUnsupported: false,
});
if (data["responseHeadersUnsupported"]) {
document
.getElementById("swf_takeover")!
.parentElement!.classList.add("hidden");
}
document.title = utils.i18n.getMessage("settings_page");
{
const vt = document.getElementById("version-text")!;
Expand Down
1 change: 1 addition & 0 deletions web/packages/extension/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const baseExtensionConfig = {
letterbox: "on" as Letterbox,
forceScale: true,
forceAlign: true,
showSwfDownload: true,
};

const swfToFlashVersion: { [key: number]: string } = {
Expand Down
8 changes: 8 additions & 0 deletions web/packages/extension/src/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ async function displayTabStatus() {
}

window.addEventListener("DOMContentLoaded", async () => {
const data = await utils.storage.sync.get({
responseHeadersUnsupported: false,
});
if (data["responseHeadersUnsupported"]) {
document
.getElementById("swf_takeover")!
.parentElement!.classList.add("hidden");
}
bindOptions((options) => {
savedOptions = options;
optionsChanged();
Expand Down
11 changes: 11 additions & 0 deletions web/packages/extension/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ const DEFAULT_OPTIONS: Required<Options> = {
ignoreOptout: false,
autostart: false,
showReloadButton: false,
swfTakeover: true,
};

// TODO: Once https://crbug.com/798169 is addressed, just use browser.
// We have to wait until whatever version of Chromium supports that
// is old enough to be the oldest version we want to support.

export let i18n: typeof browser.i18n | typeof chrome.i18n;

type ScriptingType = (typeof browser.scripting | typeof chrome.scripting) & {
Expand All @@ -28,6 +33,10 @@ export let runtime: typeof browser.runtime | typeof chrome.runtime;

export let permissions: typeof browser.permissions | typeof chrome.permissions;

export let declarativeNetRequest:
| typeof browser.declarativeNetRequest
| typeof chrome.declarativeNetRequest;

function promisify<T>(
func: (callback: (result: T) => void) => void,
): Promise<T> {
Expand All @@ -50,13 +59,15 @@ if (typeof browser !== "undefined") {
tabs = browser.tabs;
runtime = browser.runtime;
permissions = browser.permissions;
declarativeNetRequest = browser.declarativeNetRequest;
} else if (typeof chrome !== "undefined") {
i18n = chrome.i18n;
scripting = chrome.scripting as ScriptingType;
storage = chrome.storage;
tabs = chrome.tabs;
runtime = chrome.runtime;
permissions = chrome.permissions;
declarativeNetRequest = chrome.declarativeNetRequest;
} else {
throw new Error("Extension API not found.");
}
Expand Down