diff --git a/web/packages/core/src/plugin-polyfill.ts b/web/packages/core/src/plugin-polyfill.ts index b9fb4413bf77..99cec3075999 100644 --- a/web/packages/core/src/plugin-polyfill.ts +++ b/web/packages/core/src/plugin-polyfill.ts @@ -38,11 +38,13 @@ class RuffleMimeTypeArray implements MimeTypeArray { * @param mimeType The mime type to install */ install(mimeType: MimeType): void { + const wrapper = new RuffleMimeType(mimeType); + const index = this.__mimeTypes.length; - this.__mimeTypes.push(mimeType); - this.__namedMimeTypes[mimeType.type] = mimeType; - this[mimeType.type] = mimeType; - this[index] = mimeType; + this.__mimeTypes.push(wrapper); + this.__namedMimeTypes[mimeType.type] = wrapper; + this[wrapper.type] = wrapper; + this[index] = wrapper; } item(index: number): MimeType { @@ -66,6 +68,43 @@ class RuffleMimeTypeArray implements MimeTypeArray { [Symbol.iterator](): IterableIterator { return this.__mimeTypes[Symbol.iterator](); } + + get [Symbol.toStringTag](): string { + return "MimeTypeArray"; + } +} + +/** + * Replacement object for the built-in MimeType object. + * This only exists, because the built-in type is not constructable and we + * need to spoof `window.MimeType`. + */ +class RuffleMimeType implements MimeType { + private readonly __mimeType: MimeType; + + constructor(mimeType: MimeType) { + this.__mimeType = mimeType; + } + + get type(): string { + return this.__mimeType.type; + } + + get description(): string { + return this.__mimeType.description; + } + + get suffixes(): string { + return this.__mimeType.suffixes; + } + + get enabledPlugin(): Plugin { + return this.__mimeType.enabledPlugin; + } + + get [Symbol.toStringTag](): string { + return "MimeType"; + } } /** @@ -141,6 +180,10 @@ class RufflePluginArray implements PluginArray { return this.__plugins[Symbol.iterator](); } + get [Symbol.toStringTag](): string { + return "PluginArray"; + } + get length(): number { return this.__plugins.length; } @@ -205,6 +248,9 @@ export function installPlugin(plugin: RufflePlugin): void { return; } if (!("install" in navigator.plugins) || !navigator.plugins["install"]) { + Object.defineProperty(window, "PluginArray", { + value: RufflePluginArray, + }); Object.defineProperty(navigator, "plugins", { value: new RufflePluginArray(navigator.plugins), writable: false, @@ -218,6 +264,12 @@ export function installPlugin(plugin: RufflePlugin): void { plugin.length > 0 && (!("install" in navigator.mimeTypes) || !navigator.mimeTypes["install"]) ) { + Object.defineProperty(window, "MimeTypeArray", { + value: RuffleMimeTypeArray, + }); + Object.defineProperty(window, "MimeType", { + value: RuffleMimeType, + }); Object.defineProperty(navigator, "mimeTypes", { value: new RuffleMimeTypeArray(navigator.mimeTypes), writable: false, diff --git a/web/packages/selfhosted/test/polyfill/spoofing/expected.html b/web/packages/selfhosted/test/polyfill/spoofing/expected.html new file mode 100644 index 000000000000..aa8b19af6fd7 --- /dev/null +++ b/web/packages/selfhosted/test/polyfill/spoofing/expected.html @@ -0,0 +1,5 @@ + diff --git a/web/packages/selfhosted/test/polyfill/spoofing/index.html b/web/packages/selfhosted/test/polyfill/spoofing/index.html new file mode 100644 index 000000000000..ceb2648bcf9e --- /dev/null +++ b/web/packages/selfhosted/test/polyfill/spoofing/index.html @@ -0,0 +1,17 @@ + + + + + Spoofing Test + + + +
+ +
+ + diff --git a/web/packages/selfhosted/test/polyfill/spoofing/test.ts b/web/packages/selfhosted/test/polyfill/spoofing/test.ts new file mode 100644 index 000000000000..ab7816126131 --- /dev/null +++ b/web/packages/selfhosted/test/polyfill/spoofing/test.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ + +import { openTest, injectRuffleAndWait } from "../../utils.js"; +import { expect, use } from "chai"; +import chaiHtml from "chai-html"; +import fs from "fs"; + +use(chaiHtml); + +describe("Spoofing is not easily detectable", () => { + it("loads the test", async () => { + await openTest(browser, `polyfill/spoofing`); + }); + + it("Polyfills", async () => { + await injectRuffleAndWait(browser); + await browser.$("").waitForExist(); + + const actual = await browser.$("#test-container").getHTML(false); + const expected = fs.readFileSync( + `${import.meta.dirname}/expected.html`, + "utf8", + ); + expect(actual).html.to.equal(expected); + }); + + it("Spoofs navigator.plugins", async () => { + const names = await browser.execute(() => { + const names = []; + for (let i = 0; i < navigator.plugins.length; i++) { + names.push(navigator.plugins[i]!.name); + } + return names; + }); + expect(names).to.include("Shockwave Flash"); + + const instance = await browser.execute(() => { + return navigator.plugins instanceof PluginArray; + }); + expect(instance).be.true; + }); + + it("Spoofs navigator.mimeTypes", async () => { + const types = await browser.execute(() => { + const types = []; + for (let i = 0; i < navigator.mimeTypes.length; i++) { + types.push(navigator.mimeTypes[i]!.type); + } + return types; + }); + expect(types).to.include("application/x-shockwave-flash"); + + const instance = await browser.execute(() => { + for (let i = 0; i < navigator.mimeTypes.length; i++) { + if (!(navigator.mimeTypes[i] instanceof MimeType)) { + return false; + } + } + return navigator.mimeTypes instanceof MimeTypeArray; + }); + expect(instance).be.true; + }); +});