diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts index a121d105..0be74b85 100644 --- a/back/src/Model/Admin.ts +++ b/back/src/Model/Admin.ts @@ -1,9 +1,3 @@ -import { Group } from "./Group"; -import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; import { BatchMessage, PusherToBackMessage, @@ -11,7 +5,6 @@ import { ServerToClientMessage, SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage } from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; import {AdminSocket} from "../RoomManager"; diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 430dbc48..4436fb60 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -38,12 +38,10 @@ export class GameRoom { private readonly positionNotifier: PositionNotifier; public readonly roomId: string; - public readonly anonymous: boolean; - public tags: string[]; - public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; public readonly worldSlug: string = ''; public readonly organizationSlug: string = ''; + private versionNumber:number = 1; private nextUserId: number = 1; constructor(roomId: string, @@ -56,11 +54,8 @@ export class GameRoom { onLeaves: LeavesCallback) { this.roomId = roomId; - this.anonymous = isRoomAnonymous(roomId); - this.tags = []; - this.policyType = GameRoomPolicyTypes.ANONYMOUS_POLICY; - if (this.anonymous) { + if (isRoomAnonymous(roomId)) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); @@ -305,10 +300,6 @@ export class GameRoom { return this.itemsState; } - public canAccess(userTags: string[]): boolean { - return arrayIntersect(userTags, this.tags); - } - public addZoneListener(call: ZoneSocket, x: number, y: number): Set { return this.positionNotifier.addZoneListener(call, x, y); } @@ -329,4 +320,9 @@ export class GameRoom { public adminLeave(admin: Admin): void { this.admins.delete(admin); } + + public incrementVersion(): number { + this.versionNumber++ + return this.versionNumber; + } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 60e90d82..54215698 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -10,7 +10,7 @@ import { JoinRoomMessage, PlayGlobalMessage, PusherToBackMessage, - QueryJitsiJwtMessage, + QueryJitsiJwtMessage, RefreshRoomPromptMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, @@ -193,6 +193,10 @@ const roomManager: IRoomManagerServer = { socketManager.dispatchWorlFullWarning(call.request.getRoomid()); callback(null, new EmptyMessage()); }, + sendRefreshRoomPrompt(call: ServerUnaryCall, callback: sendUnaryData): void { + socketManager.dispatchRoomRefresh(call.request.getRoomid()); + callback(null, new EmptyMessage()); + }, }; export {roomManager}; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts deleted file mode 100644 index ef969a76..00000000 --- a/back/src/Services/AdminApi.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios from "axios"; - -export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] -} - -export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string -} - -class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - - const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { - organizationSlug, - worldSlug - }; - - if (roomSlug) { - params.roomSlug = roomSlug; - } - - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) - return res.data; - } -} - -export const adminApi = new AdminApi(); diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index 166622f9..4a76f131 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -26,7 +26,7 @@ import { GroupLeftZoneMessage, WorldFullWarningMessage, UserLeftZoneMessage, - BanUserMessage, + BanUserMessage, RefreshRoomMessage, } from "../Messages/generated/messages_pb"; import {User, UserSocket} from "../Model/User"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; @@ -41,7 +41,6 @@ import { } from "../Enum/EnvironmentVariable"; import {Movable} from "../Model/Movable"; import {PositionInterface} from "../Model/PositionInterface"; -import {adminApi, CharacterTexture} from "./AdminApi"; import Jwt from "jsonwebtoken"; import {JITSI_URL} from "../Enum/EnvironmentVariable"; import {clientEventsEmitter} from "./ClientEventsEmitter"; @@ -129,15 +128,7 @@ export class SocketManager { if (viewport === undefined) { throw new Error('Viewport not found in message'); } - - // sending to all clients in room except sender - /*client.position = { - x: position.x, - y: position.y, - direction, - moving: position.moving, - }; - client.viewport = viewport;*/ + // update position in the world room.updatePosition(user, ProtobufUtils.toPointInterface(position)); @@ -192,21 +183,6 @@ export class SocketManager { } } - // TODO: handle this message in pusher - /*async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { - try { - const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); - if (!reportedSocket) { - throw 'reported socket user not found'; - } - //TODO report user on admin application - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) - } catch (e) { - console.error('An error occurred on "handleReportMessage"'); - console.error(e); - } - }*/ - emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); @@ -289,11 +265,6 @@ export class SocketManager { (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener) ); - if (!world.anonymous) { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) - world.tags = data.tags - world.policyType = Number(data.policy_type) - } gaugeManager.incNbRoomGauge(); this.rooms.set(roomId, world); } @@ -774,6 +745,25 @@ export class SocketManager { recipient.socket.write(clientMessage); }); } + + dispatchRoomRefresh(roomId: string,): void { + const room = this.rooms.get(roomId); + if (!room) { + return; + } + + const versionNumber = room.incrementVersion(); + room.getUsers().forEach((recipient) => { + const worldFullMessage = new RefreshRoomMessage(); + worldFullMessage.setRoomid(roomId) + worldFullMessage.setVersionnumber(versionNumber) + + const clientMessage = new ServerToClientMessage(); + clientMessage.setRefreshroommessage(worldFullMessage); + + recipient.socket.write(clientMessage); + }); + } } export const socketManager = new SocketManager(); diff --git a/contrib/docker/docker-compose.prod.yaml b/contrib/docker/docker-compose.prod.yaml index c726ba84..6b3b8520 100644 --- a/contrib/docker/docker-compose.prod.yaml +++ b/contrib/docker/docker-compose.prod.yaml @@ -37,7 +37,7 @@ services: DEBUG_MODE: "$DEBUG_MODE" JITSI_URL: $JITSI_URL JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" - API_URL: pusher.${DOMAIN} + PUSHER_URL: //pusher.${DOMAIN} TURN_SERVER: "${TURN_SERVER}" TURN_USER: "${TURN_USER}" TURN_PASSWORD: "${TURN_PASSWORD}" diff --git a/deeployer.libsonnet b/deeployer.libsonnet index 52cea293..07506f11 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -82,9 +82,9 @@ }, "ports": [80], "env": { - "API_URL": "pusher."+url, - "UPLOADER_URL": "uploader."+url, - "ADMIN_URL": url, + "PUSHER_URL": "//pusher."+url, + "UPLOADER_URL": "//uploader."+url, + "ADMIN_URL": "//"+url, "JITSI_URL": env.JITSI_URL, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", diff --git a/docker-compose.single-domain.yaml b/docker-compose.single-domain.yaml new file mode 100644 index 00000000..0bd1dcb6 --- /dev/null +++ b/docker-compose.single-domain.yaml @@ -0,0 +1,207 @@ +version: "3" +services: + reverse-proxy: + image: traefik:v2.0 + command: + - --api.insecure=true + - --providers.docker + - --entryPoints.web.address=:80 + - --entryPoints.websecure.address=:443 + ports: + - "80:80" + - "443:443" + # The Web UI (enabled by --api.insecure=true) + - "8080:8080" + depends_on: + - back + - front + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + front: + image: thecodingmachine/nodejs:14 + environment: + DEBUG_MODE: "$DEBUG_MODE" + JITSI_URL: $JITSI_URL + JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" + HOST: "0.0.0.0" + NODE_ENV: development + PUSHER_URL: /pusher + UPLOADER_URL: /uploader + ADMIN_URL: /admin + MAPS_URL: /maps + STARTUP_COMMAND_1: yarn install + TURN_SERVER: "turn:localhost:3478,turns:localhost:5349" + # Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials. + # Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container + TURN_USER: "" + TURN_PASSWORD: "" + START_ROOM_URL: "$START_ROOM_URL" + command: yarn run start + volumes: + - ./front:/usr/src/app + labels: + - "traefik.http.routers.front.rule=PathPrefix(`/`)" + - "traefik.http.routers.front.entryPoints=web,traefik" + - "traefik.http.services.front.loadbalancer.server.port=8080" + - "traefik.http.routers.front-ssl.rule=PathPrefix(`/`)" + - "traefik.http.routers.front-ssl.entryPoints=websecure" + - "traefik.http.routers.front-ssl.tls=true" + - "traefik.http.routers.front-ssl.service=front" + + pusher: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run prod + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + SECRET_KEY: yourSecretKey + ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + API_URL: back:50051 + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + volumes: + - ./pusher:/usr/src/app + labels: + - "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher" + - "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)" + - "traefik.http.routers.pusher.middlewares=strip-pusher-prefix@docker" + - "traefik.http.routers.pusher.entryPoints=web" + - "traefik.http.services.pusher.loadbalancer.server.port=8080" + - "traefik.http.routers.pusher-ssl.rule=PathPrefix(`/pusher`)" + - "traefik.http.routers.pusher-ssl.middlewares=strip-pusher-prefix@docker" + - "traefik.http.routers.pusher-ssl.entryPoints=websecure" + - "traefik.http.routers.pusher-ssl.tls=true" + - "traefik.http.routers.pusher-ssl.service=pusher" + + maps: + image: thecodingmachine/nodejs:12-apache + environment: + DEBUG_MODE: "$DEBUG_MODE" + HOST: "0.0.0.0" + NODE_ENV: development + #APACHE_DOCUMENT_ROOT: dist/ + #APACHE_EXTENSIONS: headers + #APACHE_EXTENSION_HEADERS: 1 + STARTUP_COMMAND_0: sudo a2enmod headers + STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_2: yarn run dev & + volumes: + - ./maps:/var/www/html + labels: + - "traefik.http.middlewares.strip-maps-prefix.stripprefix.prefixes=/maps" + - "traefik.http.routers.maps.rule=PathPrefix(`/maps`)" + - "traefik.http.routers.maps.middlewares=strip-maps-prefix@docker" + - "traefik.http.routers.maps.entryPoints=web,traefik" + - "traefik.http.services.maps.loadbalancer.server.port=80" + - "traefik.http.routers.maps-ssl.rule=PathPrefix(`/maps`)" + - "traefik.http.routers.maps-ssl.middlewares=strip-maps-prefix@docker" + - "traefik.http.routers.maps-ssl.entryPoints=websecure" + - "traefik.http.routers.maps-ssl.tls=true" + - "traefik.http.routers.maps-ssl.service=maps" + + back: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + SECRET_KEY: yourSecretKey + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" + ALLOW_ARTILLERY: "true" + ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS + volumes: + - ./back:/usr/src/app + labels: + - "traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes=/api" + - "traefik.http.routers.back.rule=PathPrefix(`/api`)" + - "traefik.http.routers.back.middlewares=strip-api-prefix@docker" + - "traefik.http.routers.back.entryPoints=web" + - "traefik.http.services.back.loadbalancer.server.port=8080" + - "traefik.http.routers.back-ssl.rule=PathPrefix(`/api`)" + - "traefik.http.routers.back-ssl.middlewares=strip-api-prefix@docker" + - "traefik.http.routers.back-ssl.entryPoints=websecure" + - "traefik.http.routers.back-ssl.tls=true" + - "traefik.http.routers.back-ssl.service=back" + + uploader: + image: thecodingmachine/nodejs:12 + command: yarn dev + #command: yarn run profile + environment: + DEBUG: "*" + STARTUP_COMMAND_1: yarn install + volumes: + - ./uploader:/usr/src/app + labels: + - "traefik.http.middlewares.strip-uploader-prefix.stripprefix.prefixes=/uploader" + - "traefik.http.routers.uploader.rule=PathPrefix(`/uploader`)" + - "traefik.http.routers.uploader.middlewares=strip-uploader-prefix@docker" + - "traefik.http.routers.uploader.entryPoints=web" + - "traefik.http.services.uploader.loadbalancer.server.port=8080" + - "traefik.http.routers.uploader-ssl.rule=PathPrefix(`/uploader`)" + - "traefik.http.routers.uploader-ssl.middlewares=strip-uploader-prefix@docker" + - "traefik.http.routers.uploader-ssl.entryPoints=websecure" + - "traefik.http.routers.uploader-ssl.tls=true" + - "traefik.http.routers.uploader-ssl.service=uploader" + + website: + image: thecodingmachine/nodejs:12-apache + environment: + STARTUP_COMMAND_1: npm install + STARTUP_COMMAND_2: npm run watch & + APACHE_DOCUMENT_ROOT: dist/ + volumes: + - ./website:/var/www/html + labels: + - "traefik.http.routers.website.rule=Host(`workadventure.localhost`)" + - "traefik.http.routers.website.entryPoints=web" + - "traefik.http.services.website.loadbalancer.server.port=80" + - "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)" + - "traefik.http.routers.website-ssl.entryPoints=websecure" + - "traefik.http.routers.website-ssl.tls=true" + - "traefik.http.routers.website-ssl.service=website" + + messages: + #image: thecodingmachine/nodejs:14 + image: thecodingmachine/workadventure-back-base:latest + environment: + #STARTUP_COMMAND_0: sudo apt-get install -y inotify-tools + STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_2: yarn run proto:watch + volumes: + - ./messages:/usr/src/app + - ./back:/usr/src/back + - ./front:/usr/src/front + - ./pusher:/usr/src/pusher + +# coturn: +# image: coturn/coturn:4.5.2 +# command: +# - turnserver +# #- -c=/etc/coturn/turnserver.conf +# - --log-file=stdout +# - --external-ip=$$(detect-external-ip) +# - --listening-port=3478 +# - --min-port=10000 +# - --max-port=10010 +# - --tls-listening-port=5349 +# - --listening-ip=0.0.0.0 +# - --realm=localhost +# - --server-name=localhost +# - --lt-cred-mech +# # Enable Coturn "REST API" to validate temporary passwords. +# #- --use-auth-secret +# #- --static-auth-secret=SomeStaticAuthSecret +# #- --userdb=/var/lib/turn/turndb +# - --user=workadventure:WorkAdventure123 +# # use real-valid certificate/privatekey files +# #- --cert=/root/letsencrypt/fullchain.pem +# #- --pkey=/root/letsencrypt/privkey.pem +# network_mode: host diff --git a/docker-compose.yaml b/docker-compose.yaml index 9ea637a3..504c5b23 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -26,9 +26,9 @@ services: JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" HOST: "0.0.0.0" NODE_ENV: development - API_URL: pusher.workadventure.localhost - UPLOADER_URL: uploader.workadventure.localhost - ADMIN_URL: workadventure.localhost + PUSHER_URL: //pusher.workadventure.localhost + UPLOADER_URL: //uploader.workadventure.localhost + ADMIN_URL: //workadventure.localhost STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_2: yarn install STUN_SERVER: "stun:stun.l.google.com:19302" diff --git a/front/Dockerfile b/front/Dockerfile index 51734535..ef724e0f 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /var/www/messages COPY --chown=docker:docker messages . RUN yarn install && yarn proto -# we are rebuilding on each deploy to cope with the API_URL environment URL +# we are rebuilding on each deploy to cope with the PUSHER_URL environment URL FROM thecodingmachine/nodejs:14-apache COPY --chown=docker:docker front . diff --git a/front/src/Administration/GlobalMessageManager.ts b/front/src/Administration/GlobalMessageManager.ts index f30329d9..bc9f3cfe 100644 --- a/front/src/Administration/GlobalMessageManager.ts +++ b/front/src/Administration/GlobalMessageManager.ts @@ -1,6 +1,6 @@ import {HtmlUtils} from "./../WebRtc/HtmlUtils"; import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager"; -import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "../Connexion/RoomConnection"; import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index e1e0db0a..a0edacbc 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -1,5 +1,5 @@ import Axios from "axios"; -import {API_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "./RoomConnection"; import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels"; import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; @@ -34,7 +34,7 @@ class ConnectionManager { this.connexionType = connexionType; if(connexionType === GameConnexionTypes.register) { const organizationMemberToken = urlManager.getOrganizationToken(); - const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); + const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data); this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures); localUserStore.saveUser(this.localUser); @@ -69,15 +69,15 @@ class ConnectionManager { return Promise.resolve(new Room(roomId)); } - return Promise.reject('Invalid URL'); + return Promise.reject(new Error('Invalid URL')); } private async verifyToken(token: string): Promise { - await Axios.get(`${API_URL}/verify`, {params: {token}}); + await Axios.get(`${PUSHER_URL}/verify`, {params: {token}}); } public async anonymousLogin(isBenchmark: boolean = false): Promise { - const data = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); + const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data); this.localUser = new LocalUser(data.userUuid, data.authToken, []); if (!isBenchmark) { // In benchmark, we don't have a local storage. localUserStore.saveUser(this.localUser); diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 782d5edf..05d94440 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -1,5 +1,5 @@ import Axios from "axios"; -import {API_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL} from "../Enum/EnvironmentVariable"; export class Room { public readonly id: string; @@ -67,7 +67,7 @@ export class Room { // We have a private ID, we need to query the map URL from the server. const urlParts = this.parsePrivateUrl(this.id); - Axios.get(`${API_URL}/map`, { + Axios.get(`${PUSHER_URL}/map`, { params: urlParts }).then(({data}) => { console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 391d227d..a31b7476 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -1,4 +1,4 @@ -import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; +import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; import { BatchMessage, @@ -67,8 +67,12 @@ export class RoomConnection implements RoomConnection { * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" */ public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) { - let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); - url += '/room'; + let url = new URL(PUSHER_URL, window.location.toString()).toString(); + url = url.replace('http://', 'ws://').replace('https://', 'wss://'); + if (!url.endsWith('/')) { + url += '/'; + } + url += 'room'; url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); url += '&token='+(token ?encodeURIComponent(token):''); url += '&name='+encodeURIComponent(name); @@ -187,6 +191,8 @@ export class RoomConnection implements RoomConnection { adminMessagesService.onSendusermessage(message.getSendusermessage() as BanUserMessage); } else if (message.hasWorldfullwarningmessage()) { worldFullWarningStream.onMessage(); + } else if (message.hasRefreshroommessage()) { + //todo: implement a way to notify the user the room was refreshed. } else { throw new Error('Unknown message received'); } @@ -388,7 +394,7 @@ export class RoomConnection implements RoomConnection { public onConnectError(callback: (error: Event) => void): void { this.socket.addEventListener('error', callback) } - + public onConnect(callback: (roomConnection: OnConnectInterface) => void): void { //this.socket.addEventListener('open', callback) this.onMessage(EventMessage.CONNECT, callback); diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 844bf564..ea2434af 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,8 +1,9 @@ const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json'; -const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost"); -const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost'); -const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "workadventure.localhost"); +// For compatibility reasons with older versions, API_URL is the old host name of PUSHER_URL +const PUSHER_URL = process.env.PUSHER_URL || (process.env.API_URL ? '//'+process.env.API_URL : "//pusher.workadventure.localhost"); +const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost'; +const ADMIN_URL = process.env.ADMIN_URL || "//workadventure.localhost"; const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302"; const TURN_SERVER: string = process.env.TURN_SERVER || ""; const TURN_USER: string = process.env.TURN_USER || ''; @@ -17,7 +18,7 @@ const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new export { DEBUG_MODE, START_ROOM_URL, - API_URL, + PUSHER_URL, UPLOADER_URL, ADMIN_URL, RESOLUTION, diff --git a/front/webpack.config.js b/front/webpack.config.js index aec70d4a..f1a3df44 100644 --- a/front/webpack.config.js +++ b/front/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { devServer: { contentBase: './dist', host: '0.0.0.0', + sockPort: 80, disableHostCheck: true, historyApiFallback: { rewrites: [ @@ -68,19 +69,20 @@ module.exports = { new webpack.ProvidePlugin({ Phaser: 'phaser' }), - new webpack.EnvironmentPlugin([ - 'API_URL', - 'UPLOADER_URL', - 'ADMIN_URL', - 'DEBUG_MODE', - 'STUN_SERVER', - 'TURN_SERVER', - 'TURN_USER', - 'TURN_PASSWORD', - 'JITSI_URL', - 'JITSI_PRIVATE_MODE', - 'START_ROOM_URL' - ]) + new webpack.EnvironmentPlugin({ + 'API_URL': null, + 'PUSHER_URL': undefined, + 'UPLOADER_URL': null, + 'ADMIN_URL': null, + 'DEBUG_MODE': null, + 'STUN_SERVER': null, + 'TURN_SERVER': null, + 'TURN_USER': null, + 'TURN_PASSWORD': null, + 'JITSI_URL': null, + 'JITSI_PRIVATE_MODE': null, + 'START_ROOM_URL': null + }) ], }; diff --git a/maps/Dockerfile b/maps/Dockerfile index 33ca48b5..7fc6fd19 100644 --- a/maps/Dockerfile +++ b/maps/Dockerfile @@ -1,4 +1,3 @@ -# we are rebuilding on each deploy to cope with the API_URL environment URL FROM thecodingmachine/nodejs:12-apache COPY --chown=docker:docker . . diff --git a/messages/protos/messages.proto b/messages/protos/messages.proto index eee3be86..372f00b7 100644 --- a/messages/protos/messages.proto +++ b/messages/protos/messages.proto @@ -207,6 +207,13 @@ message WorldFullWarningMessage{ message WorldFullWarningToRoomMessage{ string roomId = 1; } +message RefreshRoomPromptMessage{ + string roomId = 1; +} +message RefreshRoomMessage{ + string roomId = 1; + int32 versionNumber = 2; +} message WorldFullMessage{ } @@ -234,6 +241,7 @@ message ServerToClientMessage { AdminRoomMessage adminRoomMessage = 14; WorldFullWarningMessage worldFullWarningMessage = 15; WorldFullMessage worldFullMessage = 16; + RefreshRoomMessage refreshRoomMessage = 17; } } @@ -402,4 +410,5 @@ service RoomManager { rpc ban(BanMessage) returns (EmptyMessage); rpc sendAdminMessageToRoom(AdminRoomMessage) returns (EmptyMessage); rpc sendWorldFullWarningToRoom(WorldFullWarningToRoomMessage) returns (EmptyMessage); + rpc sendRefreshRoomPrompt(RefreshRoomPromptMessage) returns (EmptyMessage); } diff --git a/pusher/src/Controller/AdminController.ts b/pusher/src/Controller/AdminController.ts index 3f87fcf1..74d4e792 100644 --- a/pusher/src/Controller/AdminController.ts +++ b/pusher/src/Controller/AdminController.ts @@ -2,7 +2,7 @@ import {BaseController} from "./BaseController"; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; import {apiClientRepository} from "../Services/ApiClientRepository"; -import {AdminRoomMessage, WorldFullWarningToRoomMessage} from "../Messages/generated/messages_pb"; +import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb"; export class AdminController extends BaseController{ @@ -11,6 +11,56 @@ export class AdminController extends BaseController{ super(); this.App = App; this.receiveGlobalMessagePrompt(); + this.receiveRoomEditionPrompt(); + } + + receiveRoomEditionPrompt() { + this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + res.end(); + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { + res.onAborted(() => { + console.warn('/message request was aborted'); + }) + + const token = req.getHeader('admin-token'); + const body = await res.json(); + + if (token !== ADMIN_API_TOKEN) { + console.error('Admin access refused for token: '+token) + res.writeStatus("401 Unauthorized").end('Incorrect token'); + return; + } + + try { + if (typeof body.roomId !== 'string') { + throw 'Incorrect roomId parameter' + } + const roomId: string = body.roomId; + + await apiClientRepository.getClient(roomId).then((roomClient) =>{ + return new Promise((res, rej) => { + const roomMessage = new RefreshRoomPromptMessage(); + roomMessage.setRoomid(roomId); + + roomClient.sendRefreshRoomPrompt(roomMessage, (err) => { + err ? rej(err) : res(); + }); + }); + }); + } catch (err) { + this.errorToResponse(err, res); + return; + } + + res.writeStatus("200"); + res.end('ok'); + + + }); } receiveGlobalMessagePrompt() { diff --git a/pusher/src/Controller/BaseController.ts b/pusher/src/Controller/BaseController.ts index bb500b57..91882138 100644 --- a/pusher/src/Controller/BaseController.ts +++ b/pusher/src/Controller/BaseController.ts @@ -13,8 +13,20 @@ export class BaseController { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any protected errorToResponse(e: any, res: HttpResponse): void { - console.error(e.message || "An error happened.", e?.config.url); - console.error(e.stack || 'no stack defined.'); + if (e && e.message) { + let url = e?.config?.url; + if (url !== undefined) { + url = ' for URL: '+url; + } else { + url = ''; + } + console.error('ERROR: '+e.message+url); + } else if (typeof(e) === 'string') { + console.error(e); + } + if (e.stack) { + console.error(e.stack); + } if (e.response) { res.writeStatus(e.response.status+" "+e.response.statusText); this.addCorsHeaders(res); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 55a8e032..2079548f 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -198,10 +198,10 @@ export class IoSocketController { memberMessages = userData.messages; memberTags = userData.tags; memberTextures = userData.textures; - if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { + if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { throw new Error('No correct tags') } - if (!room.anonymous && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { + if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { throw new Error('No correct member') } } catch (e) { diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index 92ff87d1..dcea5859 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -13,21 +13,22 @@ export enum GameRoomPolicyTypes { export class PusherRoom { private readonly positionNotifier: PositionDispatcher; - public readonly anonymous: boolean; + public readonly public: boolean; public tags: string[]; public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; public readonly worldSlug: string = ''; public readonly organizationSlug: string = ''; + private versionNumber: number = 1; constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { - this.anonymous = isRoomAnonymous(roomId); + this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; - if (this.anonymous) { + if (this.public) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); @@ -55,4 +56,13 @@ export class PusherRoom { public isEmpty(): boolean { return this.positionNotifier.isEmpty(); } + + public needsUpdate(versionNumber: number): boolean { + if (this.versionNumber < versionNumber) { + this.versionNumber = versionNumber; + return true; + } else { + return false; + } + } } diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index d50e2a4f..bd8edeb6 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -1,5 +1,6 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; +import {GameRoomPolicyTypes} from "_Model/PusherRoom"; export interface AdminApiData { organizationSlug: string @@ -13,6 +14,13 @@ export interface AdminApiData { textures: CharacterTexture[] } +export interface MapDetailsData { + roomSlug: string, + mapUrl: string, + policy_type: GameRoomPolicyTypes, + tags: string[], +} + export interface AdminBannedData { is_banned: boolean, message: string @@ -35,9 +43,9 @@ export interface FetchMemberDataByUuidResponse { class AdminApi { - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { + async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); + return Promise.reject(new Error('No admin backoffice set!')); } const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { @@ -60,7 +68,7 @@ class AdminApi { async fetchMemberDataByUuid(uuid: string, roomId: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); + return Promise.reject(new Error('No admin backoffice set!')); } const res = await Axios.get(ADMIN_API_URL+'/api/room/access', { params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } @@ -70,7 +78,7 @@ class AdminApi { async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); + return Promise.reject(new Error('No admin backoffice set!')); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, @@ -81,7 +89,7 @@ class AdminApi { async fetchCheckUserByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); + return Promise.reject(new Error('No admin backoffice set!')); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, @@ -104,7 +112,7 @@ class AdminApi { async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); + return Promise.reject(new Error('No admin backoffice set!')); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index d555a59c..6efd6f8d 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -22,7 +22,7 @@ import { WorldFullMessage, AdminPusherToBackMessage, ServerToAdminClientMessage, - UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage + UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage, RefreshRoomMessage } from "../Messages/generated/messages_pb"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; @@ -54,7 +54,7 @@ export interface AdminSocketData { export class SocketManager implements ZoneEventListener { - private Worlds: Map = new Map(); + private rooms: Map = new Map(); private sockets: Map = new Map(); constructor() { @@ -182,6 +182,11 @@ export class SocketManager implements ZoneEventListener { // If this is the first message sent, send back the viewport. this.handleViewport(client, viewport); } + + if (message.hasRefreshroommessage()) { + const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage; + this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()) + } // Let's pass data over from the back to the client. if (!client.disconnecting) { @@ -221,7 +226,7 @@ export class SocketManager implements ZoneEventListener { try { client.viewport = viewport; - const world = this.Worlds.get(client.roomId); + const world = this.rooms.get(client.roomId); if (!world) { console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); return; @@ -312,12 +317,12 @@ export class SocketManager implements ZoneEventListener { if (socket.roomId) { try { //user leaves room - const room: PusherRoom | undefined = this.Worlds.get(socket.roomId); + const room: PusherRoom | undefined = this.rooms.get(socket.roomId); if (room) { debug('Leaving room %s.', socket.roomId); room.leave(socket); if (room.isEmpty()) { - this.Worlds.delete(socket.roomId); + this.rooms.delete(socket.roomId); debug('Room %s is empty. Deleting.', socket.roomId); } } else { @@ -341,19 +346,23 @@ export class SocketManager implements ZoneEventListener { async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.Worlds.get(roomId) + let world = this.rooms.get(roomId) if(world === undefined){ world = new PusherRoom(roomId, this); - if (!world.anonymous) { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) - world.tags = data.tags - world.policyType = Number(data.policy_type) + if (!world.public) { + await this.updateRoomWithAdminData(world); } - this.Worlds.set(roomId, world); + this.rooms.set(roomId, world); } return Promise.resolve(world) } + public async updateRoomWithAdminData(world: PusherRoom): Promise { + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + world.tags = data.tags; + world.policyType = Number(data.policy_type); + } + emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setPlayglobalmessage(playglobalmessage); @@ -362,7 +371,7 @@ export class SocketManager implements ZoneEventListener { } public getWorlds(): Map { - return this.Worlds; + return this.rooms; } searchClientByUuid(uuid: string): ExSocketInterface | null { @@ -546,6 +555,14 @@ export class SocketManager implements ZoneEventListener { client.send(serverToClientMessage.serializeBinary().buffer, true); } + + private refreshRoomData(roomId: string, versionNumber: number): void { + const room = this.rooms.get(roomId); + //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. + if (!room || !room.needsUpdate(versionNumber)) return; + + this.updateRoomWithAdminData(room); + } } export const socketManager = new SocketManager(); diff --git a/pusher/yarn.lock b/pusher/yarn.lock index 501146cb..43f58988 100644 --- a/pusher/yarn.lock +++ b/pusher/yarn.lock @@ -3032,9 +3032,9 @@ xtend@^4.0.0: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + version "3.2.2" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696" + integrity sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ== yallist@^3.0.0, yallist@^3.0.3: version "3.1.1"