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

web: Implement URL rewrite rules #18252

Merged
merged 3 commits into from
Jan 26, 2025
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
19 changes: 19 additions & 0 deletions web/packages/core/src/internal/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,25 @@ export function configureBuilder(
builder.addGamepadButtonMapping(button, keyCode);
}
}

if (isExplicit(config.urlRewriteRules)) {
for (const [regexpOrString, replacement] of config.urlRewriteRules) {
if (regexpOrString instanceof RegExp) {
builder.addUrlRewriteRule(
regexpOrString as RegExp,
replacement,
);
} else {
const escapedString = (regexpOrString as string).replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
);
const regexp = new RegExp(`^${escapedString}$`);
const escapedReplacement = replacement.replace(/\$/g, "$$$$");
builder.addUrlRewriteRule(regexp, escapedReplacement);
}
}
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions web/packages/core/src/public/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ export const DEFAULT_CONFIG: Required<BaseLoadOptions> = {
credentialAllowList: [],
playerRuntime: PlayerRuntime.FlashPlayer,
gamepadButtonMapping: {},
urlRewriteRules: [],
};
12 changes: 12 additions & 0 deletions web/packages/core/src/public/config/load-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,18 @@ export interface BaseLoadOptions {
* @default {}
*/
gamepadButtonMapping?: Partial<Record<GamepadButton, number>>;

/**
* A set of rules that rewrite URLs in both network requests and links.
*
* They are always scanned in order, and the first one that matches is used.
* A rule either matches using a RegExp (in which case the replacement may use `$...`),
* or a string (in which case the match and the replacement are always exact).
*
* They are useful when a SWF uses an obsolete URL, in which case
* you can rewrite it to something else that works.
*/
urlRewriteRules?: Array<[RegExp | string, string]>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package {
import flash.display.MovieClip;

public class Other1 extends MovieClip {
public function Other1() {
trace("Loaded other1!");

var value:String = loaderInfo.parameters["v"];
trace("QP Value: " + value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package {
import flash.display.MovieClip;

public class Other2 extends MovieClip {
public function Other2() {
trace("Loaded other2!");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package {
import flash.display.MovieClip;
import flash.events.KeyboardEvent;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.text.TextField;
import flash.text.TextFormat;

[SWF(width="100", height="100")]
public class Test extends MovieClip {
public function Test() {
stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
addChild(createLinkText());

trace("Loaded test!");
}

private function onKeyDown(event: KeyboardEvent):void {
if (event.charCode == 65) {
var loader:Loader = new Loader();
loader.load(new URLRequest("https://example.com/other1.test1"));
addChild(loader);
}
if (event.charCode == 66) {
var loader:Loader = new Loader();
loader.load(new URLRequest("other1.test2"));
addChild(loader);
}
if (event.charCode == 67) {
var loader:Loader = new Loader();
loader.load(new URLRequest("other2.swf"));
addChild(loader);
}
}

private function createLinkText():TextField {
var text:TextField = new TextField();
text.x = -20;
text.y = -20;
text.width = 200;
text.height = 200;
text.htmlText = "<font size='100'><a href='http://www.example.com/[test]site.html'>CLICK</a></font>";
return text;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<html>

<head>
<title>url_rewrite_rules</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<script>
window.RufflePlayer = window.RufflePlayer || {};
window.RufflePlayer.config = {
"urlRewriteRules": [
// Rewriting to a relative URL
[/^https?:\/\/(example.com)\/(.*)\.test1$/, "$2.swf?v=$1/$2"],
// Rewriting to an absolute URL
[/^(.*)\.test2$/, "$1.swf?v=$1"],
// Rewriting a string (not a regex, [...], $... will do nothing)
["http://www.example.com/[test]site.html", "https://www.example.com/$1/$&"],
],
};
</script>
</head>

<body>
<div>
<object type="application/x-shockwave-flash" data="test.swf" width="100" height="100" id="objectElement"></object>
</div>
</body>

</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
getTraceOutput,
hideHardwareAccelerationModal,
injectRuffleAndWait,
openTest,
playAndMonitor,
} from "../../utils.js";
import { expect, use } from "chai";
import chaiHtml from "chai-html";

use(chaiHtml);

describe("URL Rewrite Rules", () => {
it("load the test", async () => {
await openTest(browser, "integration_tests/url_rewrite_rules");
await injectRuffleAndWait(browser);
const player = await browser.$("<ruffle-object>");
await playAndMonitor(browser, player, "Loaded test!\n");
await hideHardwareAccelerationModal(browser, player);
});

it("rewrites URL of other1 to a relative one", async () => {
const player = await browser.$("#objectElement");

await browser.execute((element) => {
const el = element as unknown as HTMLElement;
el.focus();
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: "A",
code: "A",
keyCode: 65,
bubbles: true,
}),
);
}, player);

expect(await getTraceOutput(browser, player)).to.equal(
"Loaded other1!\nQP Value: example.com/other1\n",
);
});

it("rewrites URL of other1 to an absolute one", async () => {
const player = await browser.$("#objectElement");

await browser.execute((element) => {
const el = element as unknown as HTMLElement;
el.focus();
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: "B",
code: "B",
keyCode: 66,
bubbles: true,
}),
);
}, player);

expect(await getTraceOutput(browser, player)).to.equal(
"Loaded other1!\nQP Value: http://localhost:4567/test/integration_tests/url_rewrite_rules/other1\n",
);
});

it("does not rewrite URL of other2", async () => {
const player = await browser.$("#objectElement");

await browser.execute((element) => {
const el = element as unknown as HTMLElement;
el.focus();
el.dispatchEvent(
new KeyboardEvent("keydown", {
key: "C",
code: "C",
keyCode: 67,
bubbles: true,
}),
);
}, player);

expect(await getTraceOutput(browser, player)).to.equal(
"Loaded other2!\n",
);
});

it("rewrites URL of a clicked link", async () => {
const player = await browser.$("#objectElement");

player.click();

await browser.waitUntil(
async () => {
return (await browser.getUrl()).startsWith(
"https://www.example.com",
);
},
{
timeoutMsg: "Expected window URL to change",
},
);

expect(await browser.getUrl()).to.equal(
"https://www.example.com/$1/$&",
);
});
});
25 changes: 25 additions & 0 deletions web/packages/selfhosted/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,28 @@ export function loadJsAPI(swf?: string) {
}
});
}

export async function closeAllModals(
browser: WebdriverIO.Browser,
player: ChainablePromiseElement,
) {
await browser.execute(
(modals) => {
for (const modal of modals) {
const m = modal as unknown as HTMLElement;
const cl = m.querySelector(".close-modal")! as HTMLElement;
cl.click();
}
},
await player.$$(".modal:not(.hidden)"),
);
}

export async function hideHardwareAccelerationModal(
browser: WebdriverIO.Browser,
player: ChainablePromiseElement,
) {
// Trigger it if not triggered yet
await player.moveTo();
await closeAllModals(browser, player);
}
10 changes: 9 additions & 1 deletion web/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
audio, log_adapter, storage, ui, JavascriptPlayer, RuffleHandle, SocketProxy,
RUFFLE_GLOBAL_PANIC,
};
use js_sys::Promise;
use js_sys::{Promise, RegExp};
use ruffle_core::backend::audio::{AudioBackend, NullAudioBackend};
use ruffle_core::backend::storage::{MemoryStorageBackend, StorageBackend};
use ruffle_core::backend::ui::FontDefinition;
Expand Down Expand Up @@ -63,6 +63,7 @@ pub struct RuffleInstanceBuilder {
pub(crate) default_fonts: HashMap<DefaultFont, Vec<String>>,
pub(crate) custom_fonts: Vec<(String, Vec<u8>)>,
pub(crate) gamepad_button_mapping: HashMap<GamepadButton, KeyCode>,
pub(crate) url_rewrite_rules: Vec<(RegExp, String)>,
}

impl Default for RuffleInstanceBuilder {
Expand Down Expand Up @@ -100,6 +101,7 @@ impl Default for RuffleInstanceBuilder {
default_fonts: HashMap::new(),
custom_fonts: vec![],
gamepad_button_mapping: HashMap::new(),
url_rewrite_rules: vec![],
}
}
}
Expand Down Expand Up @@ -328,6 +330,11 @@ impl RuffleInstanceBuilder {
}
}

#[wasm_bindgen(js_name = "addUrlRewriteRule")]
pub fn add_url_rewrite_rules(&mut self, regexp: RegExp, replacement: String) {
self.url_rewrite_rules.push((regexp, replacement));
}

// TODO: This should be split into two methods that either load url or load data
// Right now, that's done immediately afterwards in TS
pub async fn build(&self, parent: HtmlElement, js_player: JavascriptPlayer) -> Promise {
Expand Down Expand Up @@ -612,6 +619,7 @@ impl RuffleInstanceBuilder {
self.allow_script_access,
self.allow_networking,
self.upgrade_to_https,
self.url_rewrite_rules.clone(),
self.base_url.clone(),
log_subscriber.clone(),
self.open_url_mode,
Expand Down
Loading