diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index d9ab0a70..95400713 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -14,7 +14,6 @@ export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export const REDIS_HOST = process.env.REDIS_HOST || undefined; export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379; export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined; -export const DEBUG_IGNORE_SSL = process.env.DEBUG_IGNORE_SSL ? process.env.DEBUG_IGNORE_SSL == "true" : false; export const DEBUG_BACK_IGNORE_LOCAL = process.env.DEBUG_BACK_IGNORE_LOCAL ? process.env.DEBUG_BACK_IGNORE_LOCAL == "true" : false; export { diff --git a/back/src/Services/MapFetcher.ts b/back/src/Services/MapFetcher.ts index e85b456a..7db63523 100644 --- a/back/src/Services/MapFetcher.ts +++ b/back/src/Services/MapFetcher.ts @@ -1,13 +1,11 @@ import Axios from "axios"; -import { AxiosRequestConfig } from "axios"; -import https from "https"; import ipaddr from "ipaddr.js"; import { Resolver } from "dns"; import { promisify } from "util"; import { LocalUrlError } from "./LocalUrlError"; import { ITiledMap } from "@workadventure/tiled-map-type-guard"; import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist"; -import { DEBUG_IGNORE_SSL, DEBUG_BACK_IGNORE_LOCAL } from "../Enum/EnvironmentVariable"; +import { DEBUG_BACK_IGNORE_LOCAL } from "../Enum/EnvironmentVariable"; class MapFetcher { async fetchMap(mapUrl: string): Promise { @@ -17,18 +15,6 @@ class MapFetcher { throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map'); } - const axiosConfig: AxiosRequestConfig = { - maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger - timeout: 10000, // Timeout after 10 seconds - }; - - if (DEBUG_IGNORE_SSL) { - const agent = new https.Agent({ - rejectUnauthorized: false, - }); - axiosConfig.httpsAgent = agent; - } - // Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that // returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially // target to different servers (and one could trick Axios.get into loading resources on the internal network @@ -36,18 +22,16 @@ class MapFetcher { // We can deem this problem not that important because: // - We make sure we are only passing "GET" requests // - The result of the query is never displayed to the end user - const res = await Axios.get(mapUrl, axiosConfig); + const res = await Axios.get(mapUrl, { + maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger + timeout: 10000, // Timeout after 10 seconds + }); - try { - if (!isTiledMap(res.data)) { - //TODO fixme - //throw new Error("Invalid map format for map " + mapUrl); - console.error("Invalid map format for map " + mapUrl); - } - } catch (e) { + if (!isTiledMap(res.data)) { + //TODO fixme + //throw new Error("Invalid map format for map " + mapUrl); console.error("Invalid map format for map " + mapUrl); } - return res.data; } diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index f61c0f59..26a464d1 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -44,13 +44,22 @@ class ConnectionManager { //TODO fix me to redirect this URL by pusher if (!this._currentRoom) { - console.error("cannot get currentRoom!"); loginSceneVisibleIframeStore.set(false); return null; } - const redirectUrl = `${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}&playUri=${this._currentRoom.key}`; - window.location.assign(redirectUrl); + // also allow OIDC login without admin API by using pusher + let redirectUrl : URL; + if (this._currentRoom.iframeAuthentication) { + redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`); + } else { + // need origin if PUSHER_URL is relative (in Single-Domain-Deployment) + redirectUrl = new URL(`${PUSHER_URL}/login-screen`, (!PUSHER_URL.startsWith('http:') || !PUSHER_URL.startsWith('https:')) ? window.location.origin : undefined); + } + redirectUrl.searchParams.append("state", state); + redirectUrl.searchParams.append("nonce", nonce); + redirectUrl.searchParams.append("playUri", this._currentRoom.key); + window.location.assign(redirectUrl.toString()); return redirectUrl; } diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index b523f995..1691544b 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -18,7 +18,7 @@ export class Room { private _iframeAuthentication?: string; private _mapUrl: string | undefined; private _textures: CharacterTexture[] | undefined; - // private instance: string | undefined; + private instance: string | undefined; private readonly _search: URLSearchParams; private _contactPage: string | undefined; private _group: string | null = null; @@ -75,13 +75,12 @@ export class Room { const baseUrl = new URL(currentRoomUrl); const currentRoom = new Room(baseUrl); - // let instance: string = "global"; - // if (currentRoom.isPublic) { - // instance = currentRoom.instance as string; - //} + let instance: string = "global"; + if (currentRoom.isPublic) { + instance = currentRoom.instance as string; + } - // baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; - baseUrl.pathname = "/_/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; + baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; if (absoluteExitSceneUrl.hash) { baseUrl.hash = absoluteExitSceneUrl.hash; } @@ -119,8 +118,6 @@ export class Room { * - In a private URL: [organizationId/worldId] */ public getInstance(): string { - return ""; - /* if (this.instance !== undefined) { return this.instance; } @@ -131,12 +128,12 @@ export class Room { this.instance = match[1]; return this.instance; } else { - const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); - if (!match) throw new Error('Could not extract instance from "' + this.id + '"'); - this.instance = match[1] + "/" + match[2]; + //const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); + //if (!match) throw new Error('Could not extract instance from "' + this.id + '"'); + //this.instance = match[1] + "/" + match[2]; + this.instance = "" return this.instance; } - */ } public isDisconnected(): boolean { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 88e9bcdf..1bf18d8d 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -37,7 +37,7 @@ export class GameManager { //If player name was not set show login scene with player name //If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous) - if (!this.playerName || !localUserStore.getAuthToken()) { + if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) { return LoginSceneName; } else if (!this.characterLayers || !this.characterLayers.length) { return SelectCharacterSceneName; diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 49b1419b..33407514 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -303,17 +303,12 @@ export class GameScene extends DirtyScene { //remove loader in progress removeLoader(this); - if (this.roomUrl == localUserStore.getLastRoomUrl()) { - localUserStore.setLastRoomUrl(null); - } - //display an error scene this.scene.start(ErrorSceneName, { title: "Network error", subTitle: "An error occurred while loading resource:", message: this.originalMapUrl ?? file.src, }); - return; } }); this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); @@ -460,12 +455,6 @@ export class GameScene extends DirtyScene { //initialise map this.Map = this.add.tilemap(this.MapUrlFile); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); - - if (!this.mapFile) { - localUserStore.setLastRoomUrl(null); - throw new Error("invalid map"); - } - this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.Terrains.push( this.Map.addTilesetImage( diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 921912aa..6bba0290 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -23,7 +23,9 @@ export class LoginScene extends ResizableScene { loginSceneVisibleIframeStore.set(false); //If authentication is mandatory, push authentication iframe if ( - localUserStore.getAuthToken() == undefined + localUserStore.getAuthToken() == undefined && + gameManager.currentStartedRoom && + gameManager.currentStartedRoom.authenticationMandatory ) { connectionManager.loadOpenIDScreen(); loginSceneVisibleIframeStore.set(true); diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 6a5e6ead..f1e15db1 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -38,7 +38,7 @@ class UrlManager { } public pushRoomIdToUrl(room: Room): void { - if (window.location.pathname === room.id || room.isPublic) return; + if (window.location.pathname === room.id) return; //Set last room visited! (connected or nor, must to be saved in localstorage and cache API) localUserStore.setLastRoomUrl(room.key); const hash = window.location.hash; diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 021f518a..808cb2b3 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -5,9 +5,7 @@ import { adminApi } from "../Services/AdminApi"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { parse } from "query-string"; import { openIDClient } from "../Services/OpenIDClient"; -import { DEBUG_IGNORE_SSL, PUSHER_DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable" -import { AxiosRequestConfig } from "axios"; -import https from "https"; +import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable" export interface TokenInterface { userUuid: string; @@ -173,26 +171,16 @@ export class AuthenticateController extends BaseController { res.end(); }); - this.App.post("/anonymLogin", async (res: HttpResponse, req: HttpRequest) => { + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { console.warn("Login request was aborted"); }); - if (PUSHER_DISABLE_ANONYMOUS) { + if (DISABLE_ANONYMOUS) { res.writeStatus("403 FORBIDDEN"); res.end(); } else { - let userUuid = v4(); - - const axiosConfig: AxiosRequestConfig = {}; - - if (DEBUG_IGNORE_SSL) { - const agent = new https.Agent({ - rejectUnauthorized: false, - }); - axiosConfig.httpsAgent = agent; - } - + const userUuid = v4(); const authToken = jwtTokenManager.createAuthToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index c8c515da..92c064e2 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -26,7 +26,7 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { SocketManager, socketManager } from "../Services/SocketManager"; import { emitInBatch } from "../Services/IoSocketHelpers"; -import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; +import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"; import { Zone } from "_Model/Zone"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; import { v4 } from "uuid"; @@ -175,6 +175,11 @@ export class IoSocketController { const tokenData = token && typeof token === "string" ? jwtTokenManager.verifyJWTToken(token) : null; + + if (DISABLE_ANONYMOUS && !tokenData) { + throw new Error("Expecting token"); + } + const userIdentifier = tokenData ? tokenData.identifier : ""; let memberTags: string[] = []; diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index c3326b48..97cf2077 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -2,9 +2,9 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; import { BaseController } from "./BaseController"; import { parse } from "query-string"; import { adminApi } from "../Services/AdminApi"; -import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; -import { PusherRoom, GameRoomPolicyTypes } from "../Model/PusherRoom"; -import { MapDetailsData } from "../Services/AdminApi/MapDetailsData"; +import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"; +import { GameRoomPolicyTypes } from "../Model/PusherRoom"; +import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData"; import { socketManager } from "../Services/SocketManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { v4 } from "uuid"; @@ -65,6 +65,7 @@ export class MapController extends BaseController { tags: [], textures: [], contactPage: undefined, + authenticationMandatory : DISABLE_ANONYMOUS, } as MapDetailsData) ); @@ -88,6 +89,10 @@ export class MapController extends BaseController { } const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId); + if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) { + (mapDetails as MapDetailsData).authenticationMandatory = true; + } + res.writeStatus("200 OK"); this.addCorsHeaders(res); res.end(JSON.stringify(mapDetails)); diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 4651d8c8..88f40e4e 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -12,12 +12,11 @@ const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 808 export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling) export const FRONT_URL = process.env.FRONT_URL || "http://localhost"; -export const PUSHER_DISABLE_ANONYMOUS = process.env.PUSHER_DISABLE_ANONYMOUS ? process.env.PUSHER_DISABLE_ANONYMOUS == "true" : false; +export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "true" : false; export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false; export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ""; export const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ""; export const OIDC_CLIENT_ISSUER = process.env.OIDC_CLIENT_ISSUER || ""; -export const DEBUG_IGNORE_SSL = process.env.DEBUG_IGNORE_SSL ? process.env.DEBUG_IGNORE_SSL == "true" : false; export { SECRET_KEY, diff --git a/pusher/src/Services/AdminApi/MapDetailsData.ts b/pusher/src/Services/AdminApi/MapDetailsData.ts index 278b81bb..7a1f57ff 100644 --- a/pusher/src/Services/AdminApi/MapDetailsData.ts +++ b/pusher/src/Services/AdminApi/MapDetailsData.ts @@ -16,6 +16,7 @@ export const isMapDetailsData = new tg.IsInterface() tags: tg.isArray(tg.isString), textures: tg.isArray(isCharacterTexture), contactPage: tg.isUnion(tg.isString, tg.isUndefined), + authenticationMandatory: tg.isUnion(tg.isBoolean, tg.isUndefined), }) .get();