From 0b78eb62772129ab9df8f9e8ff97aeaa015af1be Mon Sep 17 00:00:00 2001 From: arp Date: Tue, 6 Oct 2020 15:37:00 +0200 Subject: [PATCH 01/19] temp --- back/src/Controller/AuthenticateController.ts | 18 +-- back/src/Controller/IoSocketController.ts | 136 +++++++----------- back/src/Model/{World.ts => GameRoom.ts} | 107 +------------- back/src/Model/Group.ts | 3 +- back/src/Services/AdminApi.ts | 36 +++++ back/tests/PositionNotifierTest.ts | 2 +- back/tests/WorldTest.ts | 8 +- 7 files changed, 102 insertions(+), 208 deletions(-) rename back/src/Model/{World.ts => GameRoom.ts} (68%) create mode 100644 back/src/Services/AdminApi.ts diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index aa27a468..7a8a95dd 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -3,20 +3,12 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL, SECRET_KEY, URL_ROOM_STARTED} from "../E import { uuid } from 'uuidv4'; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; -import Axios from "axios"; +import {adminApi, AdminApiData} from "../Services/AdminApi"; export interface TokenInterface { userUuid: string } -interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - userUuid: string -} - export class AuthenticateController extends BaseController { constructor(private App : TemplatedApp) { @@ -51,13 +43,7 @@ export class AuthenticateController extends BaseController { let newUrl: string|null = null; if (organizationMemberToken) { - if (!ADMIN_API_URL) { - return res.status(401).send('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 data = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ).then((res): AdminApiData => res.data); + const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); userUuid = data.userUuid; mapUrlStart = data.mapUrlStart; diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 9696748a..1591a5f1 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,23 +1,14 @@ -import * as http from "http"; -import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.." import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import Jwt, {JsonWebTokenError} from "jsonwebtoken"; +import Jwt from "jsonwebtoken"; import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {World} from "../Model/World"; +import {GameRoom} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; -import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; -import si from "systeminformation"; import {Gauge} from "prom-client"; import {TokenInterface} from "../Controller/AuthenticateController"; -import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage"; import {PointInterface} from "../Model/Websocket/PointInterface"; -import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage"; -import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface"; -import {isItemEventMessageInterface} from "../Model/Websocket/ItemEventMessage"; import {uuid} from 'uuidv4'; -import {GroupUpdateInterface} from "_Model/Websocket/GroupUpdateInterface"; import {Movable} from "../Model/Movable"; import { PositionMessage, @@ -42,14 +33,17 @@ import { SilentMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, WebRtcDisconnectMessage, PlayGlobalMessage + WebRtcStartMessage, + WebRtcDisconnectMessage, + PlayGlobalMessage, } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" +import {HttpRequest, TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; import {cpuTracker} from "../Services/CpuTracker"; +import {adminApi} from "../Services/AdminApi"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -71,7 +65,7 @@ function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { } export class IoSocketController { - private Worlds: Map = new Map(); + private Worlds: Map = new Map(); private sockets: Map = new Map(); private nbClientsGauge: Gauge; private nbClientsPerRoomGauge: Gauge; @@ -100,32 +94,11 @@ export class IoSocketController { return true; } - /** - * - * @param token - */ -/* searchClientByToken(token: string): ExSocketInterface | null { - const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[]; - for (let i = 0; i < clients.length; i++) { - const client = clients[i]; - if (client.token !== token) { - continue - } - return client; - } - return null; - }*/ - - private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> { - //console.log(socket.handshake.query.token); - - const query = parse(req.getQuery()); - - if (!query.token) { + private async getUserUuidFromToken(token: unknown): Promise { + + if (!token) { throw new Error('An authentication error happened, a user tried to connect without a token.'); } - - const token = query.token; if (typeof(token) !== "string") { throw new Error('Token is expected to be a string'); } @@ -133,22 +106,15 @@ export class IoSocketController { if(token === 'test') { if (ALLOW_ARTILLERY) { - return { - token, - userUuid: uuid() - } + return uuid(); } else { throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); } } - /*if(this.searchClientByToken(socket.handshake.query.token)){ - console.error('An authentication error happened, a user tried to connect while its token is already connected.'); - return next(new Error('Authentication error')); - }*/ - - const promise = new Promise<{ token: string, userUuid: string }>((resolve, reject) => { + return new Promise((resolve, reject) => { Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + const tokenInterface = tokenDecoded as TokenInterface; if (err) { console.error('An authentication error happened, invalid JsonWebToken.', err); reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); @@ -159,25 +125,19 @@ export class IoSocketController { reject(new Error('Empty token found.')); return; } - const tokenInterface = tokenDecoded as TokenInterface; if (!this.isValidToken(tokenInterface)) { reject(new Error('Authentication error, invalid token structure.')); return; } - resolve({ - token, - userUuid: tokenInterface.userUuid - }); + resolve(tokenInterface.userUuid); }); }); - - return promise; } ioConnection() { - this.app.ws('/*', { + this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, @@ -197,7 +157,21 @@ export class IoSocketController { }); try { - const result = await this.authenticate(req); + const query = parse(req.getQuery()); + + const moderated = query.moderated || false; + const roomId = query.roomId || null; + const token = query.token; + + + const userUuid = await this.getUserUuidFromToken(token); + + this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage); + + const isGranted = await adminApi.memberIsGrantedAccessToRoom(client.userUuid, roomId); + if (!isGranted) { + throw Error('Client cannot acces this ressource.'); + } if (upgradeAborted.aborted) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -209,8 +183,8 @@ export class IoSocketController { res.upgrade({ // Data passed here is accessible on the "websocket" socket object. url: req.getUrl(), - token: result.token, - userUuid: result.userUuid + token, + userUuid }, /* Spell these correctly */ req.getHeader('sec-websocket-key'), @@ -250,13 +224,11 @@ export class IoSocketController { console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); }, - message: (ws, arrayBuffer, isBinary) => { + message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); - if (message.hasJoinroommessage()) { - this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage); - } else if (message.hasViewportmessage()) { + if (message.hasViewportmessage()) { this.handleViewport(client, message.getViewportmessage() as ViewportMessage); } else if (message.hasUsermovesmessage()) { this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); @@ -333,26 +305,22 @@ export class IoSocketController { console.warn(message); } - private handleJoinRoom(Client: ExSocketInterface, message: JoinRoomMessage): void { + private async handleJoinRoom(client: ExSocketInterface, message: JoinRoomMessage): Promise { try { - /*if (!isJoinRoomMessageInterface(message.toObject())) { - console.log(message.toObject()) - this.emitError(Client, 'Invalid JOIN_ROOM message received: ' + message.toObject().toString()); - return; - }*/ const roomId = message.getRoomid(); - if (Client.roomId === roomId) { + if (client.roomId === roomId) { return; } + //leave previous room //this.leaveRoom(Client); // Useless now, there is only one room per connection //join new previous room - const world = this.joinRoom(Client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage)); + const gameRoom = await this.joinRoom(client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage)); - const things = world.setViewport(Client, (message.getViewport() as ViewportMessage).toObject()); + const things = gameRoom.setViewport(client, (message.getViewport() as ViewportMessage).toObject()); const roomJoinedMessage = new RoomJoinedMessage(); @@ -382,7 +350,7 @@ export class IoSocketController { } } - for (const [itemId, item] of world.getItemsState().entries()) { + for (const [itemId, item] of gameRoom.getItemsState().entries()) { const itemStateMessage = new ItemStateMessage(); itemStateMessage.setItemid(itemId); itemStateMessage.setStatejson(JSON.stringify(item)); @@ -393,8 +361,8 @@ export class IoSocketController { const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - if (!Client.disconnecting) { - Client.send(serverToClientMessage.serializeBinary().buffer, true); + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); } } catch (e) { console.error('An error occurred on "join_room" event'); @@ -600,7 +568,7 @@ export class IoSocketController { if(Client.roomId){ try { //user leave previous world - const world: World | undefined = this.Worlds.get(Client.roomId); + const world: GameRoom | undefined = this.Worlds.get(Client.roomId); if (world) { world.leave(Client); if (world.isEmpty()) { @@ -616,17 +584,17 @@ export class IoSocketController { } } - private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World { + private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom { + //join user in room - //Client.join(roomId); this.nbClientsPerRoomGauge.inc({ room: roomId }); - Client.roomId = roomId; - Client.position = position; + client.roomId = roomId; + client.position = position; //check and create new world for a room let world = this.Worlds.get(roomId) if(world === undefined){ - world = new World((user1: User, group: Group) => { + world = new GameRoom((user1: User, group: Group) => { this.joinWebRtcRoom(user1, group); }, (user1: User, group: Group) => { this.disConnectedUser(user1, group); @@ -689,10 +657,10 @@ export class IoSocketController { // Dispatch groups position to newly connected user world.getGroups().forEach((group: Group) => { - this.emitCreateUpdateGroupEvent(Client, group); + this.emitCreateUpdateGroupEvent(client, group); }); //join world - world.join(Client, Client.position); + world.join(client, client.position); return world; } @@ -882,7 +850,7 @@ export class IoSocketController { } - public getWorlds(): Map { + public getWorlds(): Map { return this.Worlds; } } diff --git a/back/src/Model/World.ts b/back/src/Model/GameRoom.ts similarity index 68% rename from back/src/Model/World.ts rename to back/src/Model/GameRoom.ts index c276d04e..1f438e61 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/GameRoom.ts @@ -1,12 +1,10 @@ -import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition"; import {PointInterface} from "./Websocket/PointInterface"; import {Group} from "./Group"; -import {Distance} from "./Distance"; import {User} from "./User"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; import {PositionInterface} from "_Model/PositionInterface"; import {Identificable} from "_Model/Websocket/Identificable"; -import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "_Model/Zone"; +import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import {PositionNotifier} from "./PositionNotifier"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {Movable} from "_Model/Movable"; @@ -14,7 +12,7 @@ import {Movable} from "_Model/Movable"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; -export class World { +export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; @@ -123,7 +121,7 @@ export class World { } else { // If the user is part of a group: // should he leave the group? - const distance = World.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); + const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), user.group.getPosition()); if (distance > this.groupRadius) { this.leaveGroup(user); } @@ -199,53 +197,19 @@ export class World { return; } - const distance = World.computeDistance(user, currentUser); // compute distance between peers. + const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. if(distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } - /*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { - // We found a user we can bind to. - return; - }*/ - /* - if(context.groups.length > 0) { - - context.groups.forEach(group => { - if(group.isPartOfGroup(userPosition)) { // Is the user in a group ? - if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player) - - // Should we split the group? (is each player reachable from the current player?) - // This is needed if - // A <==> B <==> C <===> D - // becomes A <==> B <=====> C <> D - // If C moves right, the distance between B and C is too great and we must form 2 groups - - } - } else { - // If the user is in no group - // Is there someone in a group close enough and with room in the group ? - } - }); - - } else { - // Aucun groupe n'existe donc je stock les users assez proches de moi - let dist: Distance = { - distance: distance, - first: userPosition, - second: user // TODO: convertir en messageUserPosition - } - usersToBeGroupedWith.push(dist); - } - */ }); this.groups.forEach((group: Group) => { if (group.isFull()) { return; } - const distance = World.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); + const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); if(distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; @@ -275,66 +239,7 @@ export class World { return this.itemsState; } - /*getDistancesBetweenGroupUsers(group: Group): Distance[] - { - let i = 0; - let users = group.getUsers(); - let distances: Distance[] = []; - users.forEach(function(user1, key1) { - users.forEach(function(user2, key2) { - if(key1 < key2) { - distances[i] = { - distance: World.computeDistance(user1, user2), - first: user1, - second: user2 - }; - i++; - } - }); - }); - - distances.sort(World.compareDistances); - - return distances; - } - - filterGroup(distances: Distance[], group: Group): void - { - let users = group.getUsers(); - let usersToRemove = false; - let groupTmp: MessageUserPosition[] = []; - distances.forEach(dist => { - if(dist.distance <= World.MIN_DISTANCE) { - let users = [dist.first]; - let usersbis = [dist.second] - groupTmp.push(dist.first); - groupTmp.push(dist.second); - } else { - usersToRemove = true; - } - }); - - if(usersToRemove) { - // Detecte le ou les users qui se sont fait sortir du groupe - let difference = users.filter(x => !groupTmp.includes(x)); - - // TODO : Notify users un difference that they have left the group - } - - let newgroup = new Group(groupTmp); - this.groups.push(newgroup); - } - - private static compareDistances(distA: Distance, distB: Distance): number - { - if (distA.distance < distB.distance) { - return -1; - } - if (distA.distance > distB.distance) { - return 1; - } - return 0; - }*/ + setViewport(socket : Identificable, viewport: ViewportInterface): Movable[] { const user = this.users.get(socket.userId); if(typeof user === 'undefined') { diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 16dd6cd5..9afa9764 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,7 +1,6 @@ -import { World, ConnectCallback, DisconnectCallback } from "./World"; +import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; import {PositionInterface} from "_Model/PositionInterface"; -import {uuid} from "uuidv4"; import {Movable} from "_Model/Movable"; import {PositionNotifier} from "_Model/PositionNotifier"; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts new file mode 100644 index 00000000..de28e4ef --- /dev/null +++ b/back/src/Services/AdminApi.ts @@ -0,0 +1,36 @@ +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 + userUuid: string +} + +class AdminApi { + + async fetchMemberDataByToken(organizationMemberToken: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('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, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + + async memberIsGrantedAccessToRoom(memberId: string, roomId: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + const res = await Axios.get(ADMIN_API_URL+'/api/member/'+memberId+'/is-granted-access/'+roomId, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data === true; + } +} + +export const adminApi = new AdminApi(); \ No newline at end of file diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index 253283af..0f556866 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -1,5 +1,5 @@ import "jasmine"; -import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; import {PositionNotifier} from "../src/Model/PositionNotifier"; diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 8d3b1a2d..5ab421bb 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -1,5 +1,5 @@ import "jasmine"; -import {World, ConnectCallback, DisconnectCallback } from "../src/Model/World"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; @@ -21,7 +21,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -48,7 +48,7 @@ describe("World", () => { } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -77,7 +77,7 @@ describe("World", () => { disconnectCallNumber++; } - const world = new World(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); From f542b117a8686254a43841ffd7b33de6623a6f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 6 Oct 2020 18:09:23 +0200 Subject: [PATCH 02/19] Refactoring connection to pass room info on connect --- back/src/Controller/IoSocketController.ts | 114 ++++++---- front/package.json | 2 +- front/src/Connexion/ConnectionManager.ts | 7 +- front/src/Connexion/ConnexionModels.ts | 1 + front/src/Connexion/RoomConnection.ts | 58 ++--- front/src/Phaser/Game/GameScene.ts | 255 +++++++++++----------- front/yarn.lock | 12 +- messages/messages.proto | 7 - 8 files changed, 239 insertions(+), 217 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 1591a5f1..943f949c 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -24,7 +24,6 @@ import { ItemEventMessage, ViewportMessage, ClientToServerMessage, - JoinRoomMessage, ErrorMessage, RoomJoinedMessage, ItemStateMessage, @@ -33,17 +32,17 @@ import { SilentMessage, WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, - WebRtcDisconnectMessage, + WebRtcStartMessage, + WebRtcDisconnectMessage, PlayGlobalMessage, } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {HttpRequest, TemplatedApp} from "uWebSockets.js" +import {TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; import {cpuTracker} from "../Services/CpuTracker"; -import {adminApi} from "../Services/AdminApi"; +import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -95,7 +94,7 @@ export class IoSocketController { } private async getUserUuidFromToken(token: unknown): Promise { - + if (!token) { throw new Error('An authentication error happened, a user tried to connect without a token.'); } @@ -137,8 +136,7 @@ export class IoSocketController { } ioConnection() { - this.app.ws('/room', { - + this.app.ws('/room/*', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, maxPayloadLength: 16 * 1024 * 1024, @@ -147,7 +145,6 @@ export class IoSocketController { upgrade: (res, req, context) => { //console.log('An Http connection wants to become WebSocket, URL: ' + req.getUrl() + '!'); (async () => { - /* Keep track of abortions */ const upgradeAborted = {aborted: false}; @@ -159,19 +156,32 @@ export class IoSocketController { try { const query = parse(req.getQuery()); - const moderated = query.moderated || false; - const roomId = query.roomId || null; + const roomId = req.getUrl().substr(6); + const token = query.token; + const x = Number(query.x); + const y = Number(query.y); + const top = Number(query.top); + const bottom = Number(query.bottom); + const left = Number(query.left); + const right = Number(query.right); + const name = query.name; + if (typeof name !== 'string') { + throw new Error('Expecting name'); + } + if (name === '') { + throw new Error('No empty name'); + } + let characterLayers = query.characterLayers; + if (characterLayers === null) { + throw new Error('Expecting skin'); + } + if (typeof characterLayers === 'string') { + characterLayers = [ characterLayers ]; + } const userUuid = await this.getUserUuidFromToken(token); - - this.handleJoinRoom(client, message.getJoinroommessage() as JoinRoomMessage); - - const isGranted = await adminApi.memberIsGrantedAccessToRoom(client.userUuid, roomId); - if (!isGranted) { - throw Error('Client cannot acces this ressource.'); - } if (upgradeAborted.aborted) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -184,7 +194,22 @@ export class IoSocketController { // Data passed here is accessible on the "websocket" socket object. url: req.getUrl(), token, - userUuid + userUuid, + roomId, + name, + characterLayers, + position: { + x: x, + y: y, + direction: 'down', + moving: false + } as PointInterface, + viewport: { + top, + right, + bottom, + left + } }, /* Spell these correctly */ req.getHeader('sec-websocket-key'), @@ -217,12 +242,34 @@ export class IoSocketController { emitInBatch(client, payload); } client.disconnecting = false; + + client.name = ws.name; + client.characterLayers = ws.characterLayers; + client.roomId = ws.roomId; + this.sockets.set(client.userId, client); // Let's log server load when a user joins this.nbClientsGauge.inc(); console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); + // Let's join the room + this.handleJoinRoom(client, client.roomId, client.position, client.viewport, client.name, client.characterLayers); + + /*const isGranted = await adminApi.memberIsGrantedAccessToRoom(client.userUuid, roomId); + if (!isGranted) { + throw Error('Client cannot acces this ressource.'); + }*/ + + const setUserIdMessage = new SetUserIdMessage(); + setUserIdMessage.setUserid(client.userId); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setSetuseridmessage(setUserIdMessage); + + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; @@ -305,22 +352,12 @@ export class IoSocketController { console.warn(message); } - private async handleJoinRoom(client: ExSocketInterface, message: JoinRoomMessage): Promise { + private async handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): Promise { try { - const roomId = message.getRoomid(); - - if (client.roomId === roomId) { - return; - } - - - //leave previous room - //this.leaveRoom(Client); // Useless now, there is only one room per connection - //join new previous room - const gameRoom = await this.joinRoom(client, roomId, ProtobufUtils.toPointInterface(message.getPosition() as PositionMessage)); + const gameRoom = await this.joinRoom(client, roomId, position); - const things = gameRoom.setViewport(client, (message.getViewport() as ViewportMessage).toObject()); + const things = gameRoom.setViewport(client, viewport); const roomJoinedMessage = new RoomJoinedMessage(); @@ -448,6 +485,7 @@ export class IoSocketController { } } + // Useless now, will be useful again if we allow editing details in game private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { const playerDetails = { name: playerDetailsMessage.getName(), @@ -461,16 +499,6 @@ export class IoSocketController { client.name = playerDetails.name; client.characterLayers = playerDetails.characterLayers; - - const setUserIdMessage = new SetUserIdMessage(); - setUserIdMessage.setUserid(client.userId); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setSetuseridmessage(setUserIdMessage); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - } } private handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { @@ -585,7 +613,7 @@ export class IoSocketController { } private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom { - + //join user in room this.nbClientsPerRoomGauge.inc({ room: roomId }); client.roomId = roomId; diff --git a/front/package.json b/front/package.json index c3b9f880..76e71f35 100644 --- a/front/package.json +++ b/front/package.json @@ -27,7 +27,7 @@ "google-protobuf": "^3.13.0", "phaser": "^3.22.0", "queue-typescript": "^1.0.1", - "quill": "1.3.6", + "quill": "^1.3.7", "simple-peer": "^9.6.2", "socket.io-client": "^2.3.0", "webpack-require-http": "^0.4.3" diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 4df45099..217f9e8d 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -1,6 +1,7 @@ import Axios from "axios"; import {API_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "./RoomConnection"; +import {PositionInterface, ViewportInterface} from "./ConnexionModels"; interface LoginApiData { authToken: string @@ -35,9 +36,9 @@ class ConnectionManager { this.authToken = 'test'; } - public connectToRoomSocket(): Promise { + public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.authToken as string); + const connection = new RoomConnection(this.authToken as string, roomId, name, characterLayers, position, viewport); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); @@ -50,7 +51,7 @@ class ConnectionManager { return new Promise((resolve, reject) => { setTimeout(() => { //todo: allow a way to break recurrsion? - this.connectToRoomSocket().then((connection) => resolve(connection)); + this.connectToRoomSocket(roomId, name, characterLayers, position, viewport).then((connection) => resolve(connection)); }, 4000 + Math.floor(Math.random() * 2000) ); }); }); diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index 4ec76198..3df32331 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -6,6 +6,7 @@ export enum EventMessage{ WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal", WEBRTC_START = "webrtc-start", + START_ROOM = "start-room", // From server to client: list of all room users/groups/items JOIN_ROOM = "join-room", // bi-directional USER_POSITION = "user-position", // From client to server USER_MOVED = "user-moved", // From server to client diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 3492f0f9..8799f977 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -6,7 +6,7 @@ import { GroupDeleteMessage, GroupUpdateMessage, ItemEventMessage, - JoinRoomMessage, PlayGlobalMessage, + PlayGlobalMessage, PositionMessage, RoomJoinedMessage, ServerToClientMessage, @@ -30,7 +30,7 @@ import {ProtobufClientUtils} from "../Network/ProtobufClientUtils"; import { EventMessage, GroupCreatedUpdatedMessageInterface, ItemEventMessageInterface, - MessageUserJoined, PlayGlobalMessageInterface, + MessageUserJoined, PlayGlobalMessageInterface, PositionInterface, RoomJoinedMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, @@ -48,9 +48,25 @@ export class RoomConnection implements RoomConnection { RoomConnection.websocketFactory = websocketFactory; } - public constructor(token: string) { + /** + * + * @param token A JWT token containing the UUID of the user + * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" + */ + public constructor(token: string, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); - url += '?token='+token; + url += '/room/'+roomId + url += '?token='+encodeURIComponent(token); + url += '&name='+encodeURIComponent(name); + for (let layer of characterLayers) { + url += '&characterLayers='+encodeURIComponent(layer); + } + url += '&x='+Math.floor(position.x); + url += '&y='+Math.floor(position.y); + url += '&top='+Math.floor(viewport.top); + url += '&bottom='+Math.floor(viewport.bottom); + url += '&left='+Math.floor(viewport.left); + url += '&right='+Math.floor(viewport.right); if (RoomConnection.websocketFactory) { this.socket = RoomConnection.websocketFactory(url); @@ -106,11 +122,11 @@ export class RoomConnection implements RoomConnection { items[item.getItemid()] = JSON.parse(item.getStatejson()); } - this.resolveJoinRoom({ + this.dispatch(EventMessage.START_ROOM, { users, groups, items - }) + }); } else if (message.hasSetuseridmessage()) { this.userId = (message.getSetuseridmessage() as SetUserIdMessage).getUserid(); } else if (message.hasErrormessage()) { @@ -159,29 +175,6 @@ export class RoomConnection implements RoomConnection { this.socket?.close(); } - private resolveJoinRoom!: (value?: (RoomJoinedMessageInterface | PromiseLike | undefined)) => void; - - public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise { - const promise = new Promise((resolve, reject) => { - this.resolveJoinRoom = resolve; - - const positionMessage = this.toPositionMessage(startX, startY, direction, moving); - const viewportMessage = this.toViewportMessage(viewport); - - const joinRoomMessage = new JoinRoomMessage(); - joinRoomMessage.setRoomid(roomId); - joinRoomMessage.setPosition(positionMessage); - joinRoomMessage.setViewport(viewportMessage); - - //console.log('Sending position ', positionMessage.getX(), positionMessage.getY()); - const clientToServerMessage = new ClientToServerMessage(); - clientToServerMessage.setJoinroommessage(joinRoomMessage); - - this.socket.send(clientToServerMessage.serializeBinary().buffer); - }) - return promise; - } - private toPositionMessage(x : number, y : number, direction : string, moving: boolean): PositionMessage { const positionMessage = new PositionMessage(); positionMessage.setX(Math.floor(x)); @@ -337,6 +330,13 @@ export class RoomConnection implements RoomConnection { this.socket.addEventListener('open', callback) } + /** + * Triggered when we receive all the details of a room (users, groups, ...) + */ + public onStartRoom(callback: (event: RoomJoinedMessageInterface) => void): void { + this.onMessage(EventMessage.START_ROOM, callback); + } + public sendWebrtcSignal(signal: unknown, receiverId: number) { const webRtcSignal = new WebRtcSignalToServerMessage(); webRtcSignal.setReceiverid(receiverId); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index bdaf3ac6..b3a5f104 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -107,7 +107,6 @@ export class GameScene extends Phaser.Scene implements CenterListener { private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; - private connectionPromise!: Promise private connectionAnswerPromise: Promise; private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; // A promise that will resolve when the "create" method is called (signaling loading is ended) @@ -206,106 +205,6 @@ export class GameScene extends Phaser.Scene implements CenterListener { loadAllLayers(this.load); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); - - this.connectionPromise = connectionManager.connectToRoomSocket().then((connection : RoomConnection) => { - this.connection = connection; - - this.connection.emitPlayerDetailsMessage(gameManager.getPlayerName(), gameManager.getCharacterSelected()) - - connection.onUserJoins((message: MessageUserJoined) => { - const userMessage: AddPlayerInterface = { - userId: message.userId, - characterLayers: message.characterLayers, - name: message.name, - position: message.position - } - this.addPlayer(userMessage); - }); - - connection.onUserMoved((message: UserMovedMessage) => { - const position = message.getPosition(); - if (position === undefined) { - throw new Error('Position missing from UserMovedMessage'); - } - //console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid()); - - const messageUserMoved: MessageUserMovedInterface = { - userId: message.getUserid(), - position: ProtobufClientUtils.toPointInterface(position) - } - - this.updatePlayerPosition(messageUserMoved); - }); - - connection.onUserLeft((userId: number) => { - this.removePlayer(userId); - }); - - connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { - this.shareGroupPosition(groupPositionMessage); - }) - - connection.onGroupDeleted((groupId: number) => { - try { - this.deleteGroup(groupId); - } catch (e) { - console.error(e); - } - }) - - connection.onServerDisconnected(() => { - console.log('Player disconnected from server. Reloading scene.'); - - this.simplePeer.closeAllConnections(); - this.simplePeer.unregister(); - - const key = 'somekey'+Math.round(Math.random()*10000); - const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); - this.scene.add(key, game, true, - { - initPosition: { - x: this.CurrentPlayer.x, - y: this.CurrentPlayer.y - } - }); - - this.scene.stop(this.scene.key); - this.scene.remove(this.scene.key); - window.removeEventListener('resize', this.onResizeCallback); - }) - - connection.onActionableEvent((message => { - const item = this.actionableItems.get(message.itemId); - if (item === undefined) { - console.warn('Received an event about object "'+message.itemId+'" but cannot find this item on the map.'); - return; - } - item.fire(message.event, message.state, message.parameters); - })); - - // When connection is performed, let's connect SimplePeer - this.simplePeer = new SimplePeer(this.connection); - this.GlobalMessageManager = new GlobalMessageManager(this.connection); - - const self = this; - this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { - self.presentationModeSprite.setVisible(true); - self.chatModeSprite.setVisible(true); - }, - onDisconnect(userId: number) { - if (self.simplePeer.getNbConnections() === 0) { - self.presentationModeSprite.setVisible(false); - self.chatModeSprite.setVisible(false); - } - } - }) - - this.scene.wake(); - this.scene.sleep(ReconnectingSceneName); - - return connection; - }); } // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. @@ -617,6 +516,133 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.connection.setSilent(true); } }); + + const camera = this.cameras.main; + + connectionManager.connectToRoomSocket( + this.RoomId, + gameManager.getPlayerName(), + gameManager.getCharacterSelected(), + { + x: this.startX, + y: this.startY + }, + { + left: camera.scrollX, + top: camera.scrollY, + right: camera.scrollX + camera.width, + bottom: camera.scrollY + camera.height, + }).then((connection : RoomConnection) => { + this.connection = connection; + + //this.connection.emitPlayerDetailsMessage(gameManager.getPlayerName(), gameManager.getCharacterSelected()) + connection.onStartRoom((roomJoinedMessage: RoomJoinedMessageInterface) => { + this.initUsersPosition(roomJoinedMessage.users); + this.connectionAnswerPromiseResolve(roomJoinedMessage); + }); + + connection.onUserJoins((message: MessageUserJoined) => { + const userMessage: AddPlayerInterface = { + userId: message.userId, + characterLayers: message.characterLayers, + name: message.name, + position: message.position + } + this.addPlayer(userMessage); + }); + + connection.onUserMoved((message: UserMovedMessage) => { + const position = message.getPosition(); + if (position === undefined) { + throw new Error('Position missing from UserMovedMessage'); + } + //console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid()); + + const messageUserMoved: MessageUserMovedInterface = { + userId: message.getUserid(), + position: ProtobufClientUtils.toPointInterface(position) + } + + this.updatePlayerPosition(messageUserMoved); + }); + + connection.onUserLeft((userId: number) => { + this.removePlayer(userId); + }); + + connection.onGroupUpdatedOrCreated((groupPositionMessage: GroupCreatedUpdatedMessageInterface) => { + this.shareGroupPosition(groupPositionMessage); + }) + + connection.onGroupDeleted((groupId: number) => { + try { + this.deleteGroup(groupId); + } catch (e) { + console.error(e); + } + }) + + connection.onServerDisconnected(() => { + console.log('Player disconnected from server. Reloading scene.'); + + this.simplePeer.closeAllConnections(); + this.simplePeer.unregister(); + + const key = 'somekey'+Math.round(Math.random()*10000); + const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); + this.scene.add(key, game, true, + { + initPosition: { + x: this.CurrentPlayer.x, + y: this.CurrentPlayer.y + } + }); + + this.scene.stop(this.scene.key); + this.scene.remove(this.scene.key); + window.removeEventListener('resize', this.onResizeCallback); + }) + + connection.onActionableEvent((message => { + const item = this.actionableItems.get(message.itemId); + if (item === undefined) { + console.warn('Received an event about object "'+message.itemId+'" but cannot find this item on the map.'); + return; + } + item.fire(message.event, message.state, message.parameters); + })); + + // When connection is performed, let's connect SimplePeer + this.simplePeer = new SimplePeer(this.connection); + this.GlobalMessageManager = new GlobalMessageManager(this.connection); + + const self = this; + this.simplePeer.registerPeerConnectionListener({ + onConnect(user: UserSimplePeerInterface) { + self.presentationModeSprite.setVisible(true); + self.chatModeSprite.setVisible(true); + }, + onDisconnect(userId: number) { + if (self.simplePeer.getNbConnections() === 0) { + self.presentationModeSprite.setVisible(false); + self.chatModeSprite.setVisible(false); + } + } + }) + + //listen event to share position of user + this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) + this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { + this.gameMap.setPosition(event.x, event.y); + }) + + + this.scene.wake(); + this.scene.sleep(ReconnectingSceneName); + + return connection; + }); } private switchLayoutMode(): void { @@ -787,32 +813,6 @@ export class GameScene extends Phaser.Scene implements CenterListener { //create collision this.createCollisionWithPlayer(); this.createCollisionObject(); - - //join room - this.connectionPromise.then((connection: RoomConnection) => { - const camera = this.cameras.main; - connection.joinARoom(this.RoomId, - this.startX, - this.startY, - PlayerAnimationNames.WalkDown, - false, { - left: camera.scrollX, - top: camera.scrollY, - right: camera.scrollX + camera.width, - bottom: camera.scrollY + camera.height, - }).then((roomJoinedMessage: RoomJoinedMessageInterface) => { - this.initUsersPosition(roomJoinedMessage.users); - this.connectionAnswerPromiseResolve(roomJoinedMessage); - }); - // FIXME: weirdly enough we don't use the result of joinARoom !!!!!! - - //listen event to share position of user - this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) - this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => { - this.gameMap.setPosition(event.x, event.y); - }) - }); } pushPlayerPosition(event: HasMovedEvent) { @@ -983,7 +983,6 @@ export class GameScene extends Phaser.Scene implements CenterListener { type: "InitUserPositionEvent", event: usersPosition }); - } /** diff --git a/front/yarn.lock b/front/yarn.lock index c014d18d..5d235a82 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1893,7 +1893,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.1, extend@^3.0.2: +extend@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3871,15 +3871,15 @@ quill-delta@^3.6.2: extend "^3.0.2" fast-diff "1.1.2" -quill@1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.6.tgz#99f4de1fee85925a0d7d4163b6d8328f23317a4d" - integrity sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug== +quill@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8" + integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g== dependencies: clone "^2.1.1" deep-equal "^1.0.1" eventemitter3 "^2.0.3" - extend "^3.0.1" + extend "^3.0.2" parchment "^1.1.4" quill-delta "^3.6.2" diff --git a/messages/messages.proto b/messages/messages.proto index 87cf2231..63c3ddaf 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -38,12 +38,6 @@ message SetPlayerDetailsMessage { repeated string characterLayers = 2; } -message JoinRoomMessage { - string roomId = 1; - PositionMessage position = 2; - ViewportMessage viewport = 3; -} - message UserMovesMessage { PositionMessage position = 1; ViewportMessage viewport = 2; @@ -56,7 +50,6 @@ message WebRtcSignalToServerMessage { message ClientToServerMessage { oneof message { - JoinRoomMessage joinRoomMessage = 1; UserMovesMessage userMovesMessage = 2; SilentMessage silentMessage = 3; ViewportMessage viewportMessage = 4; From 4af46b1b3fe10d51c472bdc1a9023c6495504c05 Mon Sep 17 00:00:00 2001 From: arp Date: Thu, 8 Oct 2020 18:51:24 +0200 Subject: [PATCH 03/19] simplified mapUrl parsing --- back/src/Controller/AuthenticateController.ts | 7 +- front/src/Connexion/ConnectionManager.ts | 14 +++- .../src/Phaser/Entity/GameSceneDescriptor.ts | 6 ++ front/src/Phaser/Game/GameManager.ts | 66 +++++++++++++++---- front/src/Phaser/Game/GameScene.ts | 27 ++++---- front/src/Phaser/Login/EnableCameraScene.ts | 46 ++----------- 6 files changed, 93 insertions(+), 73 deletions(-) create mode 100644 front/src/Phaser/Entity/GameSceneDescriptor.ts diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 7a8a95dd..a178b530 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -36,6 +36,7 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; + const mapSlug:string|null = param.mapSlug; try { let userUuid; @@ -48,10 +49,14 @@ export class AuthenticateController extends BaseController { userUuid = data.userUuid; mapUrlStart = data.mapUrlStart; newUrl = this.getNewUrlOnAdminAuth(data) + } else if (mapSlug !== null) { + userUuid = uuid(); + mapUrlStart = mapSlug; + newUrl = null; } else { userUuid = uuid(); mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; - newUrl = null; + newUrl = '_/global/'+mapUrlStart; } const authToken = Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 217f9e8d..a9d15dd9 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -17,10 +17,20 @@ class ConnectionManager { private authToken:string|null = null; private userUuid: string|null = null; + //todo: get map infos from url in anonym case public async init(): Promise { + let organizationMemberToken = null; + let teamSlug = null; + let mapSlug = null; const match = /\/register\/(.+)/.exec(window.location.toString()); - const organizationMemberToken = match ? match[1] : null; - this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken}).then(res => res.data); + if (match) { + organizationMemberToken = match[1]; + } else { + const match = /\/_\/(.+)\/(.+)/.exec(window.location.toString()); + teamSlug = match ? match[1] : null; + mapSlug = match ? match[2] : null; + } + this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken, teamSlug, mapSlug}).then(res => res.data); const data = await this.initPromise this.authToken = data.authToken; this.userUuid = data.userUuid; diff --git a/front/src/Phaser/Entity/GameSceneDescriptor.ts b/front/src/Phaser/Entity/GameSceneDescriptor.ts new file mode 100644 index 00000000..df114c28 --- /dev/null +++ b/front/src/Phaser/Entity/GameSceneDescriptor.ts @@ -0,0 +1,6 @@ +export class GameSceneDescriptor { + + constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { + this.roomId = '';// + } +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 960ce7e2..5188d2fe 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,4 +1,4 @@ -import {GameScene} from "./GameScene"; +import {GameScene, GameSceneInitInterface} from "./GameScene"; import { StartMapInterface } from "../../Connexion/ConnexionModels"; @@ -13,6 +13,11 @@ export interface HasMovedEvent { y: number; } +export interface loadMapResponseInterface { + key: string, + startLayerName: string; +} + export class GameManager { private playerName!: string; private characterLayers!: string[]; @@ -29,15 +34,6 @@ export class GameManager { this.characterLayers = layers; } - loadStartMap() : Promise { - return connectionManager.getMapUrlStart().then(mapUrlStart => { - return { - mapUrlStart: mapUrlStart, - startInstance: "global", //todo: is this property still usefull? - } - }); - } - getPlayerName(): string { return this.playerName; } @@ -46,8 +42,47 @@ export class GameManager { return this.characterLayers; } - loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { - const sceneKey = GameScene.getMapKeyByUrl(mapUrl); + /** + * Returns the map URL and the instance from the current URL + */ + private findMapUrl(): [string, string]|null { + const path = window.location.pathname; + if (!path.startsWith('/_/')) { + return null; + } + const instanceAndMap = path.substr(3); + const firstSlash = instanceAndMap.indexOf('/'); + if (firstSlash === -1) { + return null; + } + const instance = instanceAndMap.substr(0, firstSlash); + return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; + } + + public loadStartingMap(scene: Phaser.Scenes.ScenePlugin): Promise { + // Do we have a start URL in the address bar? If so, let's redirect to this address + const instanceAndMapUrl = this.findMapUrl(); + if (instanceAndMapUrl !== null) { + const [mapUrl, instance] = instanceAndMapUrl; + const key = gameManager.loadMap(mapUrl, scene, instance); + const startLayerName = window.location.hash ? window.location.hash.substr(1) : ''; + return Promise.resolve({key, startLayerName}); + + } else { + // If we do not have a map address in the URL, let's ask the server for a start map. + return connectionManager.getMapUrlStart().then((mapUrlStart: string) => { + const key = gameManager.loadMap(window.location.protocol + "//" + mapUrlStart, scene, 'global'); + return {key, startLayerName: ''} + }).catch((err) => { + console.error(err); + throw err; + }); + } + + } + + public loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { + const sceneKey = this.getMapKeyByUrl(mapUrl); const gameIndex = scene.getIndex(sceneKey); if(gameIndex === -1){ @@ -56,6 +91,13 @@ export class GameManager { } return sceneKey; } + + public getMapKeyByUrl(mapUrlStart: string) : string { + // FIXME: the key should be computed from the full URL of the map. + const startPos = mapUrlStart.indexOf('://')+3; + const endPos = mapUrlStart.indexOf(".json"); + return mapUrlStart.substring(startPos, endPos); + } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index b3a5f104..a9591f21 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -138,17 +138,17 @@ export class GameScene extends Phaser.Scene implements CenterListener { private outlinedItem: ActionableItem|null = null; private userInputManager!: UserInputManager; - static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { - const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); - if (key === null) { - key = mapKey; + static createFromUrl(mapUrlFile: string, instance: string, gameSceneKey: string|null = null): GameScene { + const mapKey = gameManager.getMapKeyByUrl(mapUrlFile); + if (gameSceneKey === null) { + gameSceneKey = mapKey; } - return new GameScene(mapKey, mapUrlFile, instance, key); + return new GameScene(mapKey, mapUrlFile, instance, gameSceneKey); } - constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { + constructor(MapKey : string, MapUrlFile: string, instance: string, gameSceneKey: string) { super({ - key: key + key: gameSceneKey }); this.GameManager = gameManager; @@ -588,9 +588,9 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.simplePeer.closeAllConnections(); this.simplePeer.unregister(); - const key = 'somekey'+Math.round(Math.random()*10000); - const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, key); - this.scene.add(key, game, true, + const gameSceneKey = 'somekey'+Math.round(Math.random()*10000); + const game : Phaser.Scene = GameScene.createFromUrl(this.MapUrlFile, this.instance, gameSceneKey); + this.scene.add(gameSceneKey, game, true, { initPosition: { x: this.CurrentPlayer.x, @@ -1136,12 +1136,7 @@ export class GameScene extends Phaser.Scene implements CenterListener { this.groups.delete(groupId); } - public static getMapKeyByUrl(mapUrlStart: string) : string { - // FIXME: the key should be computed from the full URL of the map. - const startPos = mapUrlStart.indexOf('://')+3; - const endPos = mapUrlStart.indexOf(".json"); - return mapUrlStart.substring(startPos, endPos); - } + /** * Sends to the server an event emitted by one of the ActionableItems. diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 6ac1ad47..672facbb 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -94,7 +94,7 @@ export class EnableCameraScene extends Phaser.Scene { this.add.existing(this.logo); this.input.keyboard.on('keyup-ENTER', () => { - return this.login(); + this.login(); }); this.getElementByIdOrFail('webRtcSetup').classList.add('active'); @@ -258,7 +258,7 @@ export class EnableCameraScene extends Phaser.Scene { this.soundMeterSprite.setVolume(this.soundMeter.getVolume()); } - private async login(): Promise { + private async login(): Promise { this.getElementByIdOrFail('webRtcSetup').style.display = 'none'; this.soundMeter.stop(); window.removeEventListener('resize', this.repositionCallback); @@ -266,46 +266,8 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - // Do we have a start URL in the address bar? If so, let's redirect to this address - const instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - const [mapUrl, instance] = instanceAndMapUrl; - const key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key, { - startLayerName: window.location.hash ? window.location.hash.substr(1) : undefined - } as GameSceneInitInterface); - return { - mapUrlStart: mapUrl, - startInstance: instance - }; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((startMap: StartMapInterface) => { - const key = gameManager.loadMap(window.location.protocol + "//" + startMap.mapUrlStart, this.scene, startMap.startInstance); - this.scene.start(key); - return startMap; - }).catch((err) => { - console.error(err); - throw err; - }); - } - } - - /** - * Returns the map URL and the instance from the current URL - */ - private findMapUrl(): [string, string]|null { - const path = window.location.pathname; - if (!path.startsWith('/_/')) { - return null; - } - const instanceAndMap = path.substr(3); - const firstSlash = instanceAndMap.indexOf('/'); - if (firstSlash === -1) { - return null; - } - const instance = instanceAndMap.substr(0, firstSlash); - return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; + let {key, startLayerName} = await gameManager.loadStartingMap(this.scene); + this.scene.start(key, {startLayerName}); } private async getDevices() { From 5e54fc2c26901810572842fbeea66bb10e983e18 Mon Sep 17 00:00:00 2001 From: arp Date: Fri, 9 Oct 2020 14:53:18 +0200 Subject: [PATCH 04/19] some fixes --- back/src/Controller/AuthenticateController.ts | 6 +- back/src/Controller/IoSocketController.ts | 68 +++---------------- back/src/Services/JWTTokenManager.ts | 60 ++++++++++++++++ front/src/Connexion/ConnectionManager.ts | 2 +- front/src/Connexion/RoomConnection.ts | 4 +- .../src/Phaser/Entity/GameSceneDescriptor.ts | 6 -- 6 files changed, 76 insertions(+), 70 deletions(-) create mode 100644 back/src/Services/JWTTokenManager.ts delete mode 100644 front/src/Phaser/Entity/GameSceneDescriptor.ts diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index a178b530..d84ccb56 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -1,9 +1,9 @@ -import Jwt from "jsonwebtoken"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." +import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import { uuid } from 'uuidv4'; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; import {adminApi, AdminApiData} from "../Services/AdminApi"; +import {jwtTokenManager} from "../Services/JWTTokenManager"; export interface TokenInterface { userUuid: string @@ -59,7 +59,7 @@ export class AuthenticateController extends BaseController { newUrl = '_/global/'+mapUrlStart; } - const authToken = Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); + const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK").end(JSON.stringify({ authToken, userUuid, diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 943f949c..30f14134 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,14 +1,11 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import Jwt from "jsonwebtoken"; -import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS, ALLOW_ARTILLERY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." +import {MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import {GameRoom} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; import {Gauge} from "prom-client"; -import {TokenInterface} from "../Controller/AuthenticateController"; import {PointInterface} from "../Model/Websocket/PointInterface"; -import {uuid} from 'uuidv4'; import {Movable} from "../Model/Movable"; import { PositionMessage, @@ -43,6 +40,8 @@ import {TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; import {cpuTracker} from "../Services/CpuTracker"; import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; +import {jwtTokenManager} from "../Services/JWTTokenManager"; +import {adminApi} from "../Services/AdminApi"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -86,54 +85,7 @@ export class IoSocketController { this.ioConnection(); } - private isValidToken(token: object): token is TokenInterface { - if (typeof((token as TokenInterface).userUuid) !== 'string') { - return false; - } - return true; - } - - private async getUserUuidFromToken(token: unknown): Promise { - - if (!token) { - throw new Error('An authentication error happened, a user tried to connect without a token.'); - } - if (typeof(token) !== "string") { - throw new Error('Token is expected to be a string'); - } - - - if(token === 'test') { - if (ALLOW_ARTILLERY) { - return uuid(); - } else { - throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); - } - } - - return new Promise((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { - const tokenInterface = tokenDecoded as TokenInterface; - if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); - return; - } - if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); - return; - } - - if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); - return; - } - - resolve(tokenInterface.userUuid); - }); - }); - } + ioConnection() { this.app.ws('/room/*', { @@ -181,7 +133,12 @@ export class IoSocketController { } - const userUuid = await this.getUserUuidFromToken(token); + const userUuid = await jwtTokenManager.getUserUuidFromToken(token); + + const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomId); + if (!isGranted) { + throw Error('Client cannot acces this ressource.'); + } if (upgradeAborted.aborted) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -256,11 +213,6 @@ export class IoSocketController { // Let's join the room this.handleJoinRoom(client, client.roomId, client.position, client.viewport, client.name, client.characterLayers); - /*const isGranted = await adminApi.memberIsGrantedAccessToRoom(client.userUuid, roomId); - if (!isGranted) { - throw Error('Client cannot acces this ressource.'); - }*/ - const setUserIdMessage = new SetUserIdMessage(); setUserIdMessage.setUserid(client.userId); diff --git a/back/src/Services/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts new file mode 100644 index 00000000..905e0ac6 --- /dev/null +++ b/back/src/Services/JWTTokenManager.ts @@ -0,0 +1,60 @@ +import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; +import {uuid} from "uuidv4"; +import Jwt from "jsonwebtoken"; +import {TokenInterface} from "../Controller/AuthenticateController"; + +class JWTTokenManager { + + public createJWTToken(userUuid: string) { + return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '24h'}); + } + + public async getUserUuidFromToken(token: unknown): Promise { + + if (!token) { + throw new Error('An authentication error happened, a user tried to connect without a token.'); + } + if (typeof(token) !== "string") { + throw new Error('Token is expected to be a string'); + } + + + if(token === 'test') { + if (ALLOW_ARTILLERY) { + return uuid(); + } else { + throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); + } + } + + return new Promise((resolve, reject) => { + Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + const tokenInterface = tokenDecoded as TokenInterface; + if (err) { + console.error('An authentication error happened, invalid JsonWebToken.', err); + reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); + return; + } + if (tokenDecoded === undefined) { + console.error('Empty token found.'); + reject(new Error('Empty token found.')); + return; + } + + if (!this.isValidToken(tokenInterface)) { + reject(new Error('Authentication error, invalid token structure.')); + return; + } + + resolve(tokenInterface.userUuid); + }); + }); + } + + private isValidToken(token: object): token is TokenInterface { + return !(typeof((token as TokenInterface).userUuid) !== 'string'); + } + +} + +export const jwtTokenManager = new JWTTokenManager(); \ No newline at end of file diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index a9d15dd9..06f8fe03 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -48,7 +48,7 @@ class ConnectionManager { public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.authToken as string, roomId, name, characterLayers, position, viewport); + const connection = new RoomConnection(this.authToken, roomId, name, characterLayers, position, viewport); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index 8799f977..f34197d3 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -53,10 +53,10 @@ export class RoomConnection implements RoomConnection { * @param token A JWT token containing the UUID of the user * @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]" */ - public constructor(token: string, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { + public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); url += '/room/'+roomId - url += '?token='+encodeURIComponent(token); + url += '?token='+(token ?encodeURIComponent(token):''); url += '&name='+encodeURIComponent(name); for (let layer of characterLayers) { url += '&characterLayers='+encodeURIComponent(layer); diff --git a/front/src/Phaser/Entity/GameSceneDescriptor.ts b/front/src/Phaser/Entity/GameSceneDescriptor.ts deleted file mode 100644 index df114c28..00000000 --- a/front/src/Phaser/Entity/GameSceneDescriptor.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class GameSceneDescriptor { - - constructor(MapKey : string, MapUrlFile: string, instance: string, key: string) { - this.roomId = '';// - } -} \ No newline at end of file From c5f8b43fec79ac7f480b72acfdd8cd89530c103a Mon Sep 17 00:00:00 2001 From: arp Date: Fri, 9 Oct 2020 16:18:25 +0200 Subject: [PATCH 05/19] more fixes --- back/src/Controller/IoSocketController.ts | 22 +++++++++++++++------- back/src/Services/AdminApi.ts | 15 ++++++++++----- front/src/Connexion/ConnectionManager.ts | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 30f14134..36a52e46 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -106,7 +106,11 @@ export class IoSocketController { }); try { + const url = req.getUrl(); const query = parse(req.getQuery()); + const websocketKey = req.getHeader('sec-websocket-key'); + const websocketProtocol = req.getHeader('sec-websocket-protocol'); + const websocketExtensions = req.getHeader('sec-websocket-extensions'); const roomId = req.getUrl().substr(6); @@ -134,10 +138,14 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); + console.log('uuid', userUuid); const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomId); if (!isGranted) { - throw Error('Client cannot acces this ressource.'); + console.log('access not granted for user '+userUuid+' and room '+roomId); + throw new Error('Client cannot acces this ressource.') + } else { + console.log('access granted for user '+userUuid+' and room '+roomId); } if (upgradeAborted.aborted) { @@ -149,7 +157,7 @@ export class IoSocketController { /* This immediately calls open handler, you must not use res after this call */ res.upgrade({ // Data passed here is accessible on the "websocket" socket object. - url: req.getUrl(), + url, token, userUuid, roomId, @@ -169,17 +177,17 @@ export class IoSocketController { } }, /* Spell these correctly */ - req.getHeader('sec-websocket-key'), - req.getHeader('sec-websocket-protocol'), - req.getHeader('sec-websocket-extensions'), + websocketKey, + websocketProtocol, + websocketExtensions, context); } catch (e) { if (e instanceof Error) { - console.warn(e.message); + console.log(e.message); res.writeStatus("401 Unauthorized").end(e.message); } else { - console.warn(e); + console.log(e); res.writeStatus("500 Internal Server Error").end('An error occurred'); } return; diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index de28e4ef..79a68810 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,5 +1,5 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios from "axios"; +import Axios, {AxiosError} from "axios"; export interface AdminApiData { organizationSlug: string @@ -26,10 +26,15 @@ class AdminApi { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); } - const res = await Axios.get(ADMIN_API_URL+'/api/member/'+memberId+'/is-granted-access/'+roomId, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - return res.data === true; + try { + const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access', + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomId} } + ) + return !!res.data; + } catch (e) { + console.log(e.message) + return false; + } } } diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 06f8fe03..7863df2f 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -36,6 +36,7 @@ class ConnectionManager { this.userUuid = data.userUuid; this.mapUrlStart = data.mapUrlStart; const newUrl = data.newUrl; + console.log('u', this.userUuid) if (newUrl) { history.pushState({}, '', newUrl); From 032facb75fe5b1d10186b26f585534a1efbd5220 Mon Sep 17 00:00:00 2001 From: arp Date: Fri, 9 Oct 2020 17:14:03 +0200 Subject: [PATCH 06/19] lint fixes --- back/src/Controller/IoSocketController.ts | 4 ++-- back/tests/WorldTest.ts | 2 +- front/src/Connexion/RoomConnection.ts | 2 +- front/src/Phaser/Game/GameScene.ts | 1 - front/src/Phaser/Login/EnableCameraScene.ts | 2 +- front/yarn.lock | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 36a52e46..7ef0d811 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -312,10 +312,10 @@ export class IoSocketController { console.warn(message); } - private async handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): Promise { + private handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): void { try { //join new previous room - const gameRoom = await this.joinRoom(client, roomId, position); + const gameRoom = this.joinRoom(client, roomId, position); const things = gameRoom.setViewport(client, viewport); diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 5ab421bb..5e06414c 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -1,5 +1,5 @@ import "jasmine"; -import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom"; +import {GameRoom, ConnectCallback, DisconnectCallback } from "../src/Model/GameRoom"; import {Point} from "../src/Model/Websocket/MessageUserPosition"; import { Group } from "../src/Model/Group"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index f4290689..ed669fed 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -59,7 +59,7 @@ export class RoomConnection implements RoomConnection { url += '/room/'+roomId url += '?token='+(token ?encodeURIComponent(token):''); url += '&name='+encodeURIComponent(name); - for (let layer of characterLayers) { + for (const layer of characterLayers) { url += '&characterLayers='+encodeURIComponent(layer); } url += '&x='+Math.floor(position.x); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 9f041eab..ba1d75fa 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -598,7 +598,6 @@ export class GameScene extends ResizableScene implements CenterListener { this.scene.stop(this.scene.key); this.scene.remove(this.scene.key); - window.removeEventListener('resize', this.onResizeCallback); }) connection.onActionableEvent((message => { diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 672facbb..5d5339d9 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -266,7 +266,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - let {key, startLayerName} = await gameManager.loadStartingMap(this.scene); + const {key, startLayerName} = await gameManager.loadStartingMap(this.scene); this.scene.start(key, {startLayerName}); } diff --git a/front/yarn.lock b/front/yarn.lock index 933a02b4..5d235a82 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -3871,7 +3871,7 @@ quill-delta@^3.6.2: extend "^3.0.2" fast-diff "1.1.2" -quill@1.3.7: +quill@^1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/quill/-/quill-1.3.7.tgz#da5b2f3a2c470e932340cdbf3668c9f21f9286e8" integrity sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g== From 02c193a262422d2ac90442f00f390d13be5b4788 Mon Sep 17 00:00:00 2001 From: arp Date: Mon, 12 Oct 2020 16:23:07 +0200 Subject: [PATCH 07/19] rewrote the authorisation flow: give more responsability to gameManager and less to gameScene --- back/src/Controller/AuthenticateController.ts | 76 ++++++++------- back/src/Controller/IoSocketController.ts | 24 +++-- back/src/Model/RoomIdentifier.ts | 14 +++ back/src/Services/AdminApi.ts | 8 +- front/src/Connexion/ConnectionManager.ts | 94 ++++++++++--------- front/src/Connexion/LocalUser.ts | 9 ++ front/src/Connexion/LocalUserStore.ts | 16 ++++ front/src/Connexion/Room.ts | 10 ++ front/src/Connexion/RoomConnection.ts | 3 +- front/src/Phaser/Game/GameManager.ts | 77 ++++----------- front/src/Phaser/Game/GameScene.ts | 23 +---- front/src/Phaser/Login/EnableCameraScene.ts | 3 +- front/src/Url/UrlManager.ts | 52 ++++++++++ front/src/index.ts | 5 +- 14 files changed, 244 insertions(+), 170 deletions(-) create mode 100644 back/src/Model/RoomIdentifier.ts create mode 100644 front/src/Connexion/LocalUser.ts create mode 100644 front/src/Connexion/LocalUserStore.ts create mode 100644 front/src/Connexion/Room.ts create mode 100644 front/src/Url/UrlManager.ts diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index 984d7445..55036a0e 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -1,8 +1,7 @@ -import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import { v4 } from 'uuid'; import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; import {BaseController} from "./BaseController"; -import {adminApi, AdminApiData} from "../Services/AdminApi"; +import {adminApi} from "../Services/AdminApi"; import {jwtTokenManager} from "../Services/JWTTokenManager"; export interface TokenInterface { @@ -13,18 +12,19 @@ export class AuthenticateController extends BaseController { constructor(private App : TemplatedApp) { super(); - this.login(); + this.register(); + this.anonymLogin(); } - //permit to login on application. Return token to connect on Websocket IO. - login(){ - this.App.options("/login", (res: HttpResponse, req: HttpRequest) => { + //Try to login with an admin token + register(){ + this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); res.end(); }); - this.App.post("/login", (res: HttpResponse, req: HttpRequest) => { + this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { this.addCorsHeaders(res); @@ -36,35 +36,25 @@ export class AuthenticateController extends BaseController { //todo: what to do if the organizationMemberToken is already used? const organizationMemberToken:string|null = param.organizationMemberToken; - const mapSlug:string|null = param.mapSlug; - + try { - let userUuid; - let mapUrlStart; - let newUrl: string|null = null; + if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); + const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); - if (organizationMemberToken) { - const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); - - userUuid = data.userUuid; - mapUrlStart = data.mapUrlStart; - newUrl = this.getNewUrlOnAdminAuth(data) - } else if (mapSlug !== null) { - userUuid = v4(); - mapUrlStart = mapSlug; - newUrl = null; - } else { - userUuid = v4(); - mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; - newUrl = '_/global/'+mapUrlStart; - } + const userUuid = data.userUuid; + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + const mapUrlStart = data.mapUrlStart; const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK").end(JSON.stringify({ authToken, userUuid, + organizationSlug, + worldSlug, + roomSlug, mapUrlStart, - newUrl, })); } catch (e) { @@ -75,12 +65,32 @@ export class AuthenticateController extends BaseController { })(); }); + } - private getNewUrlOnAdminAuth(data:AdminApiData): string { - const organizationSlug = data.organizationSlug; - const worldSlug = data.worldSlug; - const roomSlug = data.roomSlug; - return '/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug; + //permit to login on application. Return token to connect on Websocket IO. + anonymLogin(){ + this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.addCorsHeaders(res); + + res.end(); + }); + + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + (async () => { + this.addCorsHeaders(res); + + res.onAborted(() => { + console.warn('Login request was aborted'); + }) + + const userUuid = v4(); + const authToken = jwtTokenManager.createJWTToken(userUuid); + res.writeStatus("200 OK").end(JSON.stringify({ + authToken, + userUuid, + })); + })(); + }); } } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 7ef0d811..306b874e 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -42,6 +42,7 @@ import {cpuTracker} from "../Services/CpuTracker"; import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi} from "../Services/AdminApi"; +import {RoomIdentifier} from "../Model/RoomIdentifier"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -88,7 +89,7 @@ export class IoSocketController { ioConnection() { - this.app.ws('/room/*', { + this.app.ws('/room', { /* Options */ //compression: uWS.SHARED_COMPRESSOR, maxPayloadLength: 16 * 1024 * 1024, @@ -112,7 +113,12 @@ export class IoSocketController { const websocketProtocol = req.getHeader('sec-websocket-protocol'); const websocketExtensions = req.getHeader('sec-websocket-extensions'); - const roomId = req.getUrl().substr(6); + const roomId = query.roomId; + //todo: better validation: /\/_\/.*\/.*/ or /\/@\/.*\/.*\/.*/ + if (typeof roomId !== 'string') { + throw new Error('Undefined room ID: '); + } + const roomIdentifier = new RoomIdentifier(roomId); const token = query.token; const x = Number(query.x); @@ -140,12 +146,14 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); console.log('uuid', userUuid); - const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomId); - if (!isGranted) { - console.log('access not granted for user '+userUuid+' and room '+roomId); - throw new Error('Client cannot acces this ressource.') - } else { - console.log('access granted for user '+userUuid+' and room '+roomId); + if (roomIdentifier.anonymous === false) { + const isGranted = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomIdentifier); + if (!isGranted) { + console.log('access not granted for user '+userUuid+' and room '+roomId); + throw new Error('Client cannot acces this ressource.') + } else { + console.log('access granted for user '+userUuid+' and room '+roomId); + } } if (upgradeAborted.aborted) { diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts new file mode 100644 index 00000000..9bb58bb9 --- /dev/null +++ b/back/src/Model/RoomIdentifier.ts @@ -0,0 +1,14 @@ +export class RoomIdentifier { + public anonymous: boolean; + public id:string + constructor(roomID: string) { + if (roomID.indexOf('_/') === 0) { + this.anonymous = true; + } else if(roomID.indexOf('@/') === 0) { + this.anonymous = false; + } else { + throw new Error('Incorrect room ID: '+roomID); + } + this.id = roomID; //todo: extract more data from the id (like room slug, organization name, etc); + } +} \ No newline at end of file diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 79a68810..2d03ee9d 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,5 +1,6 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios, {AxiosError} from "axios"; +import Axios from "axios"; +import {RoomIdentifier} from "../Model/RoomIdentifier"; export interface AdminApiData { organizationSlug: string @@ -22,13 +23,14 @@ class AdminApi { return res.data; } - async memberIsGrantedAccessToRoom(memberId: string, roomId: string): Promise { + async memberIsGrantedAccessToRoom(memberId: string, roomIdentifier: RoomIdentifier): Promise { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); } try { + //todo: send more specialized data instead of the whole id const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access', - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomId} } + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, roomIdentifier: roomIdentifier.id} } ) return !!res.data; } catch (e) { diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 91a42882..0fea50b5 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -2,54 +2,67 @@ import Axios from "axios"; import {API_URL} from "../Enum/EnvironmentVariable"; import {RoomConnection} from "./RoomConnection"; import {PositionInterface, ViewportInterface} from "./ConnexionModels"; - -interface LoginApiData { - authToken: string - userUuid: string - mapUrlStart: string - newUrl: string -} +import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; +import {localUserStore} from "./LocalUserStore"; +import {LocalUser} from "./LocalUser"; +import {Room} from "./Room"; class ConnectionManager { - private initPromise!: Promise; - private mapUrlStart: string|null = null; + private localUser!:LocalUser; - private authToken:string|null = null; - private userUuid: string|null = null; + /** + * Tries to login to the node server and return the starting map url to be loaded + */ + public async initGameConnexion(): Promise { - //todo: get map infos from url in anonym case - public async init(): Promise { - let organizationMemberToken = null; - let teamSlug = null; - let mapSlug = null; - const match = /\/register\/(.+)/.exec(window.location.toString()); - if (match) { - organizationMemberToken = match[1]; - } else { - const match = /\/_\/(.+)\/(.+)/.exec(window.location.toString()); - teamSlug = match ? match[1] : null; - mapSlug = match ? match[2] : null; - } - this.initPromise = Axios.post(`${API_URL}/login`, {organizationMemberToken, teamSlug, mapSlug}).then(res => res.data); - const data = await this.initPromise - this.authToken = data.authToken; - this.userUuid = data.userUuid; - this.mapUrlStart = data.mapUrlStart; - const newUrl = data.newUrl; - console.log('u', this.userUuid) - - if (newUrl) { - history.pushState({}, '', newUrl); + const connexionType = urlManager.getGameConnexionType(); + if(connexionType === GameConnexionTypes.register) { + const organizationMemberToken = urlManager.getOrganizationToken(); + const data:any = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data); + this.localUser = new LocalUser(data.userUuid, data.authToken); + localUserStore.saveUser(this.localUser); + + const organizationSlug = data.organizationSlug; + const worldSlug = data.worldSlug; + const roomSlug = data.roomSlug; + urlManager.editUrlForRoom(roomSlug, organizationSlug, worldSlug); + + const room = new Room(window.location.pathname, data.mapUrlStart) + return Promise.resolve(room); + } else if (connexionType === GameConnexionTypes.anonymous) { + const localUser = localUserStore.getLocalUser(); + + if (localUser) { + this.localUser = localUser + } else { + const data:any = await Axios.post(`${API_URL}/anonymLogin`).then(res => res.data); + this.localUser = new LocalUser(data.userUuid, data.authToken); + localUserStore.saveUser(this.localUser); + } + const room = new Room(window.location.pathname, urlManager.getAnonymousMapUrlStart()) + return Promise.resolve(room); + } else if (connexionType == GameConnexionTypes.organization) { + const localUser = localUserStore.getLocalUser(); + + if (localUser) { + this.localUser = localUser + //todo: ask the node api for the correct starting map Url from its slug + return Promise.reject('Case not handled: need to get the map\'s url from its slug'); + } else { + //todo: find some kind of fallback? + return Promise.reject('Could not find a user in localstorage'); + } } + return Promise.reject('ConnexionManager initialization failed'); } public initBenchmark(): void { - this.authToken = 'test'; + this.localUser = new LocalUser('', 'test'); } public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise { return new Promise((resolve, reject) => { - const connection = new RoomConnection(this.authToken, roomId, name, characterLayers, position, viewport); + const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport); connection.onConnectError((error: object) => { console.log('An error occurred while connecting to socket server. Retrying'); reject(error); @@ -67,15 +80,6 @@ class ConnectionManager { }); }); } - - public getMapUrlStart(): Promise { - return this.initPromise.then(() => { - if (!this.mapUrlStart) { - throw new Error('No map url set!'); - } - return this.mapUrlStart; - }) - } } export const connectionManager = new ConnectionManager(); diff --git a/front/src/Connexion/LocalUser.ts b/front/src/Connexion/LocalUser.ts new file mode 100644 index 00000000..1411f66c --- /dev/null +++ b/front/src/Connexion/LocalUser.ts @@ -0,0 +1,9 @@ +export class LocalUser { + public uuid: string; + public jwtToken: string; + + constructor(uuid:string, jwtToken: string) { + this.uuid = uuid; + this.jwtToken = jwtToken; + } +} \ No newline at end of file diff --git a/front/src/Connexion/LocalUserStore.ts b/front/src/Connexion/LocalUserStore.ts new file mode 100644 index 00000000..0976b5c9 --- /dev/null +++ b/front/src/Connexion/LocalUserStore.ts @@ -0,0 +1,16 @@ +import {LocalUser} from "./LocalUser"; + +class LocalUserStore { + + saveUser(localUser: LocalUser) { + localStorage.setItem('localUser', JSON.stringify(localUser)); + } + + getLocalUser(): LocalUser|null { + const data = localStorage.getItem('localUser'); + return data ? JSON.parse(data) : null; + } + +} + +export const localUserStore = new LocalUserStore(); \ No newline at end of file diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts new file mode 100644 index 00000000..36a8072b --- /dev/null +++ b/front/src/Connexion/Room.ts @@ -0,0 +1,10 @@ +export class Room { + public ID: string; + public url: string + + constructor(ID: string, url: string) { + this.ID = ID; + this.url = url; + } + +} \ No newline at end of file diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index ed669fed..fd9410c4 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -56,7 +56,8 @@ export class RoomConnection implements RoomConnection { */ public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) { let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://'); - url += '/room/'+roomId + url += '/room'; + url += '?roomId='+(roomId ?encodeURIComponent(roomId):''); url += '?token='+(token ?encodeURIComponent(token):''); url += '&name='+encodeURIComponent(name); for (const layer of characterLayers) { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 5188d2fe..10277e20 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,10 +1,6 @@ -import {GameScene, GameSceneInitInterface} from "./GameScene"; -import { - StartMapInterface -} from "../../Connexion/ConnexionModels"; -import Axios from "axios"; -import {API_URL} from "../../Enum/EnvironmentVariable"; +import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; +import {Room} from "../../Connexion/Room"; export interface HasMovedEvent { direction: string; @@ -13,14 +9,17 @@ export interface HasMovedEvent { y: number; } -export interface loadMapResponseInterface { - key: string, - startLayerName: string; -} - export class GameManager { private playerName!: string; private characterLayers!: string[]; + private startRoom!:Room; + private sceneManager!: Phaser.Scenes.SceneManager; + + public async init(sceneManager: Phaser.Scenes.SceneManager) { + this.sceneManager = sceneManager; + this.startRoom = await connectionManager.initGameConnexion(); + this.loadMap(this.startRoom.url, this.startRoom.ID); + } public setPlayerName(name: string): void { this.playerName = name; @@ -41,55 +40,15 @@ export class GameManager { getCharacterSelected(): string[] { return this.characterLayers; } - - /** - * Returns the map URL and the instance from the current URL - */ - private findMapUrl(): [string, string]|null { - const path = window.location.pathname; - if (!path.startsWith('/_/')) { - return null; - } - const instanceAndMap = path.substr(3); - const firstSlash = instanceAndMap.indexOf('/'); - if (firstSlash === -1) { - return null; - } - const instance = instanceAndMap.substr(0, firstSlash); - return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; - } - public loadStartingMap(scene: Phaser.Scenes.ScenePlugin): Promise { - // Do we have a start URL in the address bar? If so, let's redirect to this address - const instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - const [mapUrl, instance] = instanceAndMapUrl; - const key = gameManager.loadMap(mapUrl, scene, instance); - const startLayerName = window.location.hash ? window.location.hash.substr(1) : ''; - return Promise.resolve({key, startLayerName}); - - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return connectionManager.getMapUrlStart().then((mapUrlStart: string) => { - const key = gameManager.loadMap(window.location.protocol + "//" + mapUrlStart, scene, 'global'); - return {key, startLayerName: ''} - }).catch((err) => { - console.error(err); - throw err; - }); - } - - } - - public loadMap(mapUrl: string, scene: Phaser.Scenes.ScenePlugin, instance: string): string { - const sceneKey = this.getMapKeyByUrl(mapUrl); - - const gameIndex = scene.getIndex(sceneKey); + + public loadMap(mapUrl: string, roomID: string): void { + console.log('Loading map '+roomID+' at url '+mapUrl); + const gameIndex = this.sceneManager.getIndex(roomID); if(gameIndex === -1){ - const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, instance); - scene.add(sceneKey, game, false); + const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, roomID); + this.sceneManager.add(roomID, game, false); } - return sceneKey; } public getMapKeyByUrl(mapUrlStart: string) : string { @@ -98,6 +57,10 @@ export class GameManager { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } + + public async goToStartingMap() { + this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); + } } export const gameManager = new GameManager(); diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index ba1d75fa..602f697e 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -418,15 +418,7 @@ export class GameScene extends ResizableScene implements CenterListener { context.strokeStyle = '#ffffff'; context.stroke(); this.circleTexture.refresh(); - - // Let's alter browser history - const url = new URL(this.MapUrlFile); - let path = '/_/'+this.instance+'/'+url.host+url.pathname; - if (this.startLayerName) { - path += '#'+this.startLayerName; - } - window.history.pushState({}, 'WorkAdventure', path); - + // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking @@ -686,6 +678,7 @@ export class GameScene extends ResizableScene implements CenterListener { * @param tileWidth * @param tileHeight */ + //todo: push that into the gameManager private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){ const exitSceneUrl = this.getExitSceneUrl(layer); if (exitSceneUrl === undefined) { @@ -698,7 +691,8 @@ export class GameScene extends ResizableScene implements CenterListener { // TODO: eventually compute a relative URL const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - const exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance); + gameManager.loadMap(absoluteExitSceneUrl, instance); + const exitSceneKey = instance; const tiles : number[] = layer.data as number[]; for (let key=0; key < tiles.length; key++) { @@ -785,14 +779,6 @@ export class GameScene extends ResizableScene implements CenterListener { }); } - createCollisionObject(){ - /*this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => { - this.physics.add.collider(this.CurrentPlayer, Object, (object1, object2) => { - this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key) - }); - })*/ - } - createCurrentPlayer(){ //initialise player //TODO create animation moving between exit and start @@ -809,7 +795,6 @@ export class GameScene extends ResizableScene implements CenterListener { //create collision this.createCollisionWithPlayer(); - this.createCollisionObject(); } pushPlayerPosition(event: HasMovedEvent) { diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 5d5339d9..8695464b 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -266,8 +266,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - const {key, startLayerName} = await gameManager.loadStartingMap(this.scene); - this.scene.start(key, {startLayerName}); + gameManager.goToStartingMap(); } private async getDevices() { diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts new file mode 100644 index 00000000..39f5667d --- /dev/null +++ b/front/src/Url/UrlManager.ts @@ -0,0 +1,52 @@ + +export enum GameConnexionTypes { + anonymous=1, + organization, + register, + unknown, +} + +//this class is responsible with analysing and editing the game's url +class UrlManager { + + //todo: use that to detect if we can find a token in localstorage + public getGameConnexionType(): GameConnexionTypes { + const url = window.location.pathname.toString(); + if (url.indexOf('_/') > -1) { + return GameConnexionTypes.anonymous; + } else if (url.indexOf('@/') > -1) { + return GameConnexionTypes.organization; + } else if(url.indexOf('register/')) { + return GameConnexionTypes.register + } else { + return GameConnexionTypes.unknown + } + } + + public getAnonymousMapUrlStart():string { + const match = /\/_\/global\/(.+)/.exec(window.location.pathname.toString()) + if (!match) throw new Error('Could not extract startmap url from'+window.location.pathname); + return match[1]; + + } + + public getOrganizationToken(): string|null { + const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); + return match ? match [1] : null; + } + + + public editUrlForRoom(roomSlug: string, organizationSlug: string|null, worldSlug: string |null): string { + let newUrl:string; + if (organizationSlug) { + newUrl = '/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug; + } else { + newUrl = '/_/global/'+roomSlug; + } + history.pushState({}, 'WorkAdventure', newUrl); + return newUrl; + } + +} + +export const urlManager = new UrlManager(); \ No newline at end of file diff --git a/front/src/index.ts b/front/src/index.ts index 177c56c0..f57474d7 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -11,11 +11,10 @@ import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer; import {OutlinePipeline} from "./Phaser/Shaders/OutlinePipeline"; import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; -import {connectionManager} from "./Connexion/ConnectionManager"; +import {gameManager} from "./Phaser/Game/GameManager"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; //CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); -connectionManager.init(); // Load Jitsi if the environment variable is set. if (JITSI_URL) { @@ -52,6 +51,8 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); +gameManager.init(game.scene); + window.addEventListener('resize', function (event) { const {width, height} = CoWebsiteManager.getGameSize(); From 2852f204f506caa38b438caeac7c92338c584840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 12 Oct 2020 17:42:37 +0200 Subject: [PATCH 08/19] Improving error handling upon unknown URL --- front/src/Connexion/ConnectionManager.ts | 2 +- front/src/Phaser/Game/GameManager.ts | 17 ++++++++++---- .../src/Phaser/Reconnecting/FourOFourScene.ts | 23 +++++++++++++++---- front/src/Url/UrlManager.ts | 2 +- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 0fea50b5..615f75b9 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -53,7 +53,7 @@ class ConnectionManager { return Promise.reject('Could not find a user in localstorage'); } } - return Promise.reject('ConnexionManager initialization failed'); + return Promise.reject('ConnexionManager initialization failed: invalid URL'); } public initBenchmark(): void { diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 10277e20..bed098ae 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,7 @@ import {GameScene} from "./GameScene"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {Room} from "../../Connexion/Room"; +import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; export interface HasMovedEvent { direction: string; @@ -14,10 +15,16 @@ export class GameManager { private characterLayers!: string[]; private startRoom!:Room; private sceneManager!: Phaser.Scenes.SceneManager; - + public async init(sceneManager: Phaser.Scenes.SceneManager) { this.sceneManager = sceneManager; - this.startRoom = await connectionManager.initGameConnexion(); + try { + this.startRoom = await connectionManager.initGameConnexion(); + } catch (e) { + this.sceneManager.start(FourOFourSceneName, { + url: window.location.pathname.toString() + }); + } this.loadMap(this.startRoom.url, this.startRoom.ID); } @@ -40,8 +47,8 @@ export class GameManager { getCharacterSelected(): string[] { return this.characterLayers; } - - + + public loadMap(mapUrl: string, roomID: string): void { console.log('Loading map '+roomID+' at url '+mapUrl); const gameIndex = this.sceneManager.getIndex(roomID); @@ -57,7 +64,7 @@ export class GameManager { const endPos = mapUrlStart.indexOf(".json"); return mapUrlStart.substring(startPos, endPos); } - + public async goToStartingMap() { this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); } diff --git a/front/src/Phaser/Reconnecting/FourOFourScene.ts b/front/src/Phaser/Reconnecting/FourOFourScene.ts index 0c91a5bc..3e84b7e9 100644 --- a/front/src/Phaser/Reconnecting/FourOFourScene.ts +++ b/front/src/Phaser/Reconnecting/FourOFourScene.ts @@ -15,7 +15,8 @@ export class FourOFourScene extends Phaser.Scene { private fileNameField!: Text; private logo!: Image; private cat!: Sprite; - private file!: string; + private file: string|undefined; + private url: string|undefined; constructor() { super({ @@ -23,8 +24,9 @@ export class FourOFourScene extends Phaser.Scene { }); } - init({ file }: { file: string }) { + init({ file, url }: { file?: string, url?: string }) { this.file = file; + this.url = url; } preload() { @@ -45,11 +47,22 @@ export class FourOFourScene extends Phaser.Scene { this.mapNotFoundField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "404 - File not found"); this.mapNotFoundField.setOrigin(0.5, 0.5).setCenterAlign(); - this.couldNotFindField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, "Could not load file"); + let text: string = ''; + if (this.file !== undefined) { + text = "Could not load map" + } + if (this.url !== undefined) { + text = "Invalid URL" + } + + this.couldNotFindField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, text); this.couldNotFindField.setOrigin(0.5, 0.5).setCenterAlign(); - this.fileNameField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 38, this.file, { fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif', fontSize: '10px' }); - this.fileNameField.setOrigin(0.5, 0.5); + const url = this.file ? this.file : this.url; + if (url !== undefined) { + this.fileNameField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 38, url, { fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif', fontSize: '10px' }); + this.fileNameField.setOrigin(0.5, 0.5); + } this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6); this.cat.flipY=true; diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 39f5667d..876e258e 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -16,7 +16,7 @@ class UrlManager { return GameConnexionTypes.anonymous; } else if (url.indexOf('@/') > -1) { return GameConnexionTypes.organization; - } else if(url.indexOf('register/')) { + } else if(url.indexOf('register/') > -1) { return GameConnexionTypes.register } else { return GameConnexionTypes.unknown From 0731bd39e500b181f527cceaae2f6ddaf0a295a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Mon, 12 Oct 2020 18:59:49 +0200 Subject: [PATCH 09/19] Moving back to using ScenePlugin and adding EntryScene --- front/src/Phaser/Game/GameManager.ts | 26 ++++++-------- front/src/Phaser/Game/GameScene.ts | 10 +++--- front/src/Phaser/Login/EnableCameraScene.ts | 2 +- front/src/Phaser/Login/EntryScene.ts | 40 +++++++++++++++++++++ front/src/Url/UrlManager.ts | 14 ++++---- front/src/index.ts | 5 ++- 6 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 front/src/Phaser/Login/EntryScene.ts diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index bed098ae..22123d1c 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -14,18 +14,10 @@ export class GameManager { private playerName!: string; private characterLayers!: string[]; private startRoom!:Room; - private sceneManager!: Phaser.Scenes.SceneManager; - public async init(sceneManager: Phaser.Scenes.SceneManager) { - this.sceneManager = sceneManager; - try { - this.startRoom = await connectionManager.initGameConnexion(); - } catch (e) { - this.sceneManager.start(FourOFourSceneName, { - url: window.location.pathname.toString() - }); - } - this.loadMap(this.startRoom.url, this.startRoom.ID); + public async init(scenePlugin: Phaser.Scenes.ScenePlugin) { + this.startRoom = await connectionManager.initGameConnexion(); + this.loadMap(this.startRoom.url, this.startRoom.ID, scenePlugin); } public setPlayerName(name: string): void { @@ -49,12 +41,13 @@ export class GameManager { } - public loadMap(mapUrl: string, roomID: string): void { + public loadMap(mapUrl: string, roomID: string, scenePlugin: Phaser.Scenes.ScenePlugin): void { console.log('Loading map '+roomID+' at url '+mapUrl); - const gameIndex = this.sceneManager.getIndex(roomID); + const gameIndex = scenePlugin.getIndex(mapUrl); if(gameIndex === -1){ const game : Phaser.Scene = GameScene.createFromUrl(mapUrl, roomID); - this.sceneManager.add(roomID, game, false); + console.log('Adding scene '+mapUrl); + scenePlugin.add(mapUrl, game, false); } } @@ -65,8 +58,9 @@ export class GameManager { return mapUrlStart.substring(startPos, endPos); } - public async goToStartingMap() { - this.sceneManager.start(this.startRoom.ID, {startLayerName: 'global'}); + public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) { + console.log('Starting scene '+this.startRoom.url); + scenePlugin.start(this.startRoom.url, {startLayerName: 'global'}); } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 602f697e..a22d973a 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -139,11 +139,11 @@ export class GameScene extends ResizableScene implements CenterListener { private userInputManager!: UserInputManager; static createFromUrl(mapUrlFile: string, instance: string, gameSceneKey: string|null = null): GameScene { - const mapKey = gameManager.getMapKeyByUrl(mapUrlFile); + // We use the map URL as a key if (gameSceneKey === null) { - gameSceneKey = mapKey; + gameSceneKey = mapUrlFile; } - return new GameScene(mapKey, mapUrlFile, instance, gameSceneKey); + return new GameScene(mapUrlFile, mapUrlFile, instance, gameSceneKey); } constructor(MapKey : string, MapUrlFile: string, instance: string, gameSceneKey: string) { @@ -418,7 +418,7 @@ export class GameScene extends ResizableScene implements CenterListener { context.strokeStyle = '#ffffff'; context.stroke(); this.circleTexture.refresh(); - + // Let's pause the scene if the connection is not established yet if (this.connection === undefined) { // Let's wait 0.5 seconds before printing the "connecting" screen to avoid blinking @@ -691,7 +691,7 @@ export class GameScene extends ResizableScene implements CenterListener { // TODO: eventually compute a relative URL const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; - gameManager.loadMap(absoluteExitSceneUrl, instance); + gameManager.loadMap(absoluteExitSceneUrl, instance, this.scene); const exitSceneKey = instance; const tiles : number[] = layer.data as number[]; diff --git a/front/src/Phaser/Login/EnableCameraScene.ts b/front/src/Phaser/Login/EnableCameraScene.ts index 8695464b..3916587a 100644 --- a/front/src/Phaser/Login/EnableCameraScene.ts +++ b/front/src/Phaser/Login/EnableCameraScene.ts @@ -266,7 +266,7 @@ export class EnableCameraScene extends Phaser.Scene { mediaManager.stopCamera(); mediaManager.stopMicrophone(); - gameManager.goToStartingMap(); + gameManager.goToStartingMap(this.scene); } private async getDevices() { diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts new file mode 100644 index 00000000..fec4e880 --- /dev/null +++ b/front/src/Phaser/Login/EntryScene.ts @@ -0,0 +1,40 @@ +import {gameManager} from "../Game/GameManager"; +import {TextField} from "../Components/TextField"; +import {TextInput} from "../Components/TextInput"; +import {ClickButton} from "../Components/ClickButton"; +import Image = Phaser.GameObjects.Image; +import Rectangle = Phaser.GameObjects.Rectangle; +import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; +import {cypressAsserter} from "../../Cypress/CypressAsserter"; +import {SelectCharacterSceneName} from "./SelectCharacterScene"; +import {ResizableScene} from "./ResizableScene"; +import {Scene} from "phaser"; +import {LoginSceneName} from "./LoginScene"; +import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; + +export const EntrySceneName = "EntryScene"; + +/** + * The EntryScene is not a real scene. It is the first scene loaded and is only used to initialize the gameManager + * and to route to the next correct scene. + */ +export class EntryScene extends Scene { + constructor() { + super({ + key: EntrySceneName + }); + } + + preload() { + } + + create() { + gameManager.init(this.scene).then(() => { + this.scene.start(LoginSceneName); + }).catch(() => { + this.scene.start(FourOFourSceneName, { + url: window.location.pathname.toString() + }); + }); + } +} diff --git a/front/src/Url/UrlManager.ts b/front/src/Url/UrlManager.ts index 876e258e..ae8725bc 100644 --- a/front/src/Url/UrlManager.ts +++ b/front/src/Url/UrlManager.ts @@ -8,7 +8,7 @@ export enum GameConnexionTypes { //this class is responsible with analysing and editing the game's url class UrlManager { - + //todo: use that to detect if we can find a token in localstorage public getGameConnexionType(): GameConnexionTypes { const url = window.location.pathname.toString(); @@ -22,14 +22,14 @@ class UrlManager { return GameConnexionTypes.unknown } } - + public getAnonymousMapUrlStart():string { const match = /\/_\/global\/(.+)/.exec(window.location.pathname.toString()) if (!match) throw new Error('Could not extract startmap url from'+window.location.pathname); - return match[1]; - + return window.location.protocol+'//'+match[1]; + } - + public getOrganizationToken(): string|null { const match = /\/register\/(.+)/.exec(window.location.pathname.toString()); return match ? match [1] : null; @@ -46,7 +46,7 @@ class UrlManager { history.pushState({}, 'WorkAdventure', newUrl); return newUrl; } - + } -export const urlManager = new UrlManager(); \ No newline at end of file +export const urlManager = new UrlManager(); diff --git a/front/src/index.ts b/front/src/index.ts index f57474d7..e12d8707 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -13,6 +13,7 @@ import {CustomizeScene} from "./Phaser/Login/CustomizeScene"; import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager"; import {gameManager} from "./Phaser/Game/GameManager"; import {ResizableScene} from "./Phaser/Login/ResizableScene"; +import {EntryScene} from "./Phaser/Login/EntryScene"; //CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com'); @@ -30,7 +31,7 @@ const config: GameConfig = { width: width / RESOLUTION, height: height / RESOLUTION, parent: "game", - scene: [LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], + scene: [EntryScene, LoginScene, SelectCharacterScene, EnableCameraScene, ReconnectingScene, FourOFourScene, CustomizeScene], zoom: RESOLUTION, physics: { default: "arcade", @@ -51,8 +52,6 @@ cypressAsserter.gameStarted(); const game = new Phaser.Game(config); -gameManager.init(game.scene); - window.addEventListener('resize', function (event) { const {width, height} = CoWebsiteManager.getGameSize(); From 9a04836215ad27239c73ccf3166844cb6c7b6c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Tue, 13 Oct 2020 10:26:27 +0200 Subject: [PATCH 10/19] Dynamically import Quill We load Quill only if it is needed (after all, only admins need Quill) --- front/dist/index.html | 6 - front/package.json | 3 +- .../ConsoleGlobalMessageManager.ts | 57 +- front/yarn.lock | 578 +++++++++--------- 4 files changed, 324 insertions(+), 320 deletions(-) diff --git a/front/dist/index.html b/front/dist/index.html index 9d883ffe..5984af7b 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -6,12 +6,6 @@ content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> - - - - - -