From 6fbf165c91a18ba6e902545c48f946e48b6c2a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Sun, 7 Mar 2021 21:02:38 +0100 Subject: [PATCH] Adding the ability to register a single script using the "script" attribute at the map property level. --- front/Dockerfile | 5 ++ front/dist/iframe.html | 17 +++++++ front/src/Api/IframeListener.ts | 67 ++++++++++++++++++++++++++ front/src/Phaser/Game/GameScene.ts | 29 ++++++++++- front/src/Phaser/Map/ITiledMap.ts | 2 +- maps/tests/script.js | 8 ++++ maps/tests/script_api.json | 77 ++++++++++++++++++++++++++++++ 7 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 front/dist/iframe.html create mode 100644 maps/tests/script.js create mode 100644 maps/tests/script_api.json diff --git a/front/Dockerfile b/front/Dockerfile index b0d17877..51734535 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -8,6 +8,11 @@ FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . COPY --from=builder --chown=docker:docker /var/www/messages/generated /var/www/html/src/Messages/generated + +# Removing the iframe.html file from the final image as this adds a XSS attack. +# iframe.html is only in dev mode to circumvent a limitation +RUN rm dist/iframe.html + RUN yarn install ENV NODE_ENV=production diff --git a/front/dist/iframe.html b/front/dist/iframe.html new file mode 100644 index 00000000..c8fafb4b --- /dev/null +++ b/front/dist/iframe.html @@ -0,0 +1,17 @@ + + + + + + + diff --git a/front/src/Api/IframeListener.ts b/front/src/Api/IframeListener.ts index e91b92f3..66ea312f 100644 --- a/front/src/Api/IframeListener.ts +++ b/front/src/Api/IframeListener.ts @@ -2,6 +2,8 @@ import {Subject} from "rxjs"; import {ChatEvent, isChatEvent} from "./Events/ChatEvent"; import {IframeEvent, isIframeEventWrapper} from "./Events/IframeEvent"; import {UserInputChatEvent} from "./Events/UserInputChatEvent"; +import * as crypto from "crypto"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; @@ -14,6 +16,7 @@ class IframeListener { public readonly chatStream = this._chatStream.asObservable(); private readonly iframes = new Set(); + private readonly scripts = new Map(); init() { window.addEventListener("message", (message) => { @@ -54,6 +57,70 @@ class IframeListener { this.iframes.delete(iframe); } + registerScript(scriptUrl: string): void { + console.log('Loading map related script at ', scriptUrl) + + if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + // Using external iframe mode ( + const iframe = document.createElement('iframe'); + iframe.id = this.getIFrameId(scriptUrl); + iframe.style.display = 'none'; + iframe.src = '/iframe.html?script='+encodeURIComponent(scriptUrl); + + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add('allow-scripts'); + iframe.sandbox.add('allow-top-navigation-by-user-activation'); + + document.body.prepend(iframe); + + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } else { + // production code + const iframe = document.createElement('iframe'); + iframe.id = this.getIFrameId(scriptUrl); + + // We are putting a sandbox on this script because it will run in the same domain as the main website. + iframe.sandbox.add('allow-scripts'); + iframe.sandbox.add('allow-top-navigation-by-user-activation'); + + const html = '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n'; + + //iframe.src = "data:text/html;charset=utf-8," + escape(html); + iframe.srcdoc = html; + + document.body.prepend(iframe); + + this.scripts.set(scriptUrl, iframe); + this.registerIframe(iframe); + } + + + } + + private getIFrameId(scriptUrl: string): string { + return 'script'+crypto.createHash('md5').update(scriptUrl).digest("hex"); + } + + unregisterScript(scriptUrl: string): void { + const iFrameId = this.getIFrameId(scriptUrl); + const iframe = HtmlUtils.getElementByIdOrFail(iFrameId); + if (!iframe) { + throw new Error('Unknown iframe for script "'+scriptUrl+'"'); + } + this.unregisterIframe(iframe); + iframe.remove(); + + this.scripts.delete(scriptUrl); + } + sendUserInputChat(message: string) { this.postMessage({ 'type': 'userInputChat', diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 5ca3c749..e9725db0 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -72,6 +72,7 @@ import {TextureError} from "../../Exception/TextureError"; import {addLoader} from "../Components/Loader"; import {ErrorSceneName} from "../Reconnecting/ErrorScene"; import {localUserStore} from "../../Connexion/LocalUserStore"; +import {iframeListener} from "../../Api/IframeListener"; export interface GameSceneInitInterface { initPosition: PointInterface|null, @@ -313,6 +314,12 @@ export class GameScene extends ResizableScene implements CenterListener { // }); // }); } + + // Now, let's load the script, if any + const scripts = this.getScriptUrls(this.mapFile); + for (const script of scripts) { + iframeListener.registerScript(script); + } } //hook initialisation @@ -744,6 +751,12 @@ export class GameScene extends ResizableScene implements CenterListener { public cleanupClosingScene(): void { // stop playing audio, close any open website, stop any open Jitsi coWebsiteManager.closeCoWebsite(); + // Stop the script, if any + const scripts = this.getScriptUrls(this.mapFile); + for (const script of scripts) { + iframeListener.unregisterScript(script); + } + this.stopJitsi(); this.playAudio(undefined); // We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map. @@ -829,8 +842,12 @@ export class GameScene extends ResizableScene implements CenterListener { return this.getProperty(layer, "startLayer") == true; } - private getProperty(layer: ITiledMapLayer, name: string): string|boolean|number|undefined { - const properties = layer.properties; + private getScriptUrls(map: ITiledMap): string[] { + return (this.getProperties(map, "script") as string[]).map((script) => (new URL(script, this.MapUrlFile)).toString()); + } + + private getProperty(layer: ITiledMapLayer|ITiledMap, name: string): string|boolean|number|undefined { + const properties: ITiledMapLayerProperty[] = layer.properties; if (!properties) { return undefined; } @@ -841,6 +858,14 @@ export class GameScene extends ResizableScene implements CenterListener { return obj.value; } + private getProperties(layer: ITiledMapLayer|ITiledMap, name: string): (string|number|boolean|undefined)[] { + const properties: ITiledMapLayerProperty[] = layer.properties; + if (!properties) { + return []; + } + return properties.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()).map((property) => property.value); + } + //todo: push that into the gameManager private async loadNextGame(exitSceneIdentifier: string){ const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance); diff --git a/front/src/Phaser/Map/ITiledMap.ts b/front/src/Phaser/Map/ITiledMap.ts index 2a82e93a..39e0a1f5 100644 --- a/front/src/Phaser/Map/ITiledMap.ts +++ b/front/src/Phaser/Map/ITiledMap.ts @@ -14,7 +14,7 @@ export interface ITiledMap { * Map orientation (orthogonal) */ orientation: string; - properties: {[key: string]: string}; + properties: ITiledMapLayerProperty[]; /** * Render order (right-down) diff --git a/maps/tests/script.js b/maps/tests/script.js new file mode 100644 index 00000000..dd608bca --- /dev/null +++ b/maps/tests/script.js @@ -0,0 +1,8 @@ +console.log('SCRIPT LAUNCHED'); +WA.sendChatMessage('Hi, my name is Poly and I repeat what you say!', 'Poly Parrot'); + + +WA.onChatMessage((message => { + console.log('CHAT MESSAGE RECEIVED BY SCRIPT'); + WA.sendChatMessage('Poly Parrot says: "'+message+'"', 'Poly Parrot'); +})); diff --git a/maps/tests/script_api.json b/maps/tests/script_api.json new file mode 100644 index 00000000..d578f4be --- /dev/null +++ b/maps/tests/script_api.json @@ -0,0 +1,77 @@ +{ "compressionlevel":-1, + "editorsettings": + { + "export": + { + "target":"." + } + }, + "height":10, + "infinite":false, + "layers":[ + { + "data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "height":10, + "id":1, + "name":"floor", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "height":10, + "id":2, + "name":"start", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":10, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":3, + "name":"floorLayer", + "objects":[], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":6, + "nextobjectid":1, + "orientation":"orthogonal", + "properties":[ + { + "name":"script", + "type":"string", + "value":"script.js" + }], + "renderorder":"right-down", + "tiledversion":"1.3.3", + "tileheight":32, + "tilesets":[ + { + "columns":11, + "firstgid":1, + "image":"tileset1.png", + "imageheight":352, + "imagewidth":352, + "margin":0, + "name":"tileset1", + "spacing":0, + "tilecount":121, + "tileheight":32, + "tilewidth":32 + }], + "tilewidth":32, + "type":"map", + "version":1.2, + "width":10 +} \ No newline at end of file