diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 6b112e9e..79e4ae59 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,6 +1,6 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import {MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {GameRoom} from "../Model/GameRoom"; +import {GameRoom, GameRoomPolicyTypes} from "../Model/GameRoom"; import {Group} from "../Model/Group"; import {User} from "../Model/User"; import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; @@ -41,7 +41,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"; +import {PositionInterface} from "../Model/PositionInterface"; function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -113,11 +113,9 @@ export class IoSocketController { const websocketExtensions = req.getHeader('sec-websocket-extensions'); 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); @@ -143,17 +141,20 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); - console.log('uuid', userUuid); let memberTags: string[] = []; - if (roomIdentifier.anonymous === false) { - const grants = await adminApi.memberIsGrantedAccessToRoom(userUuid, roomIdentifier); - if (!grants.granted) { + const room = await this.getOrCreateRoom(roomId); + if (!room.anonymous && room.policyType !== GameRoomPolicyTypes.ANONYMUS_POLICY) { + try { + const userData = await adminApi.fetchMemberDataByUuid(userUuid); + memberTags = userData.tags; + if (room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) { + throw new Error('No correct tags') + } + console.log('access granted for user '+userUuid+' and room '+roomId); + } catch (e) { console.log('access not granted for user '+userUuid+' and room '+roomId); throw new Error('Client cannot acces this ressource.') - } else { - memberTags = grants.memberTags; - console.log('access granted for user '+userUuid+' and room '+roomId); } } @@ -172,6 +173,7 @@ export class IoSocketController { roomId, name, characterLayers, + tags: memberTags, position: { x: x, y: y, @@ -183,8 +185,7 @@ export class IoSocketController { right, bottom, left - }, - tags: memberTags + } }, /* Spell these correctly */ websocketKey, @@ -219,9 +220,9 @@ export class IoSocketController { client.disconnecting = false; client.name = ws.name; + client.tags = ws.tags; client.characterLayers = ws.characterLayers; client.roomId = ws.roomId; - client.tags = ws.tags; this.sockets.set(client.userId, client); @@ -230,7 +231,7 @@ export class IoSocketController { 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); + this.handleJoinRoom(client, client.position, client.viewport); }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; @@ -266,11 +267,6 @@ export class IoSocketController { Client.disconnecting = true; //leave room this.leaveRoom(Client); - - //delete all socket information - /*delete Client.roomId; - delete Client.token; - delete Client.position;*/ } catch (e) { console.error('An error occurred on "disconnect"'); console.error(e); @@ -283,21 +279,6 @@ export class IoSocketController { console.log('A user left (', this.sockets.size, ' connected users)'); } }) - - // TODO: finish this! - /*this.Io.on(SocketIoEvent.CONNECTION, (socket: Socket) => { - - - - socket.on(SocketIoEvent.WEBRTC_SIGNAL, (data: unknown) => { - this.emitVideo((socket as ExSocketInterface), data); - }); - - socket.on(SocketIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, (data: unknown) => { - this.emitScreenSharing((socket as ExSocketInterface), data); - }); - - });*/ } private emitError(Client: ExSocketInterface, message: string): void { @@ -313,10 +294,10 @@ export class IoSocketController { console.warn(message); } - private handleJoinRoom(client: ExSocketInterface, roomId: string, position: PointInterface, viewport: ViewportInterface, name: string, characterLayers: string[]): void { + private handleJoinRoom(client: ExSocketInterface, position: PointInterface, viewport: ViewportInterface): void { try { //join new previous room - const gameRoom = this.joinRoom(client, roomId, position); + const gameRoom = this.joinRoom(client, position); const things = gameRoom.setViewport(client, viewport); @@ -357,7 +338,6 @@ export class IoSocketController { } roomJoinedMessage.setCurrentuserid(client.userId); - roomJoinedMessage.setTagList(client.tags); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); @@ -575,77 +555,42 @@ export class IoSocketController { } } } - - private joinRoom(client : ExSocketInterface, roomId: string, position: PointInterface): GameRoom { - - //join user in room - this.nbClientsPerRoomGauge.inc({ room: roomId }); - client.roomId = roomId; - client.position = position; - + + private async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room let world = this.Worlds.get(roomId) if(world === undefined){ - world = new GameRoom((user1: User, group: Group) => { - this.joinWebRtcRoom(user1, group); - }, (user1: User, group: Group) => { - this.disConnectedUser(user1, group); - }, MINIMUM_DISTANCE, GROUP_RADIUS, (thing: Movable, listener: User) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userJoinedMessage = new UserJoinedMessage(); - if (!Number.isInteger(clientUser.userId)) { - throw new Error('clientUser.userId is not an integer '+clientUser.userId); - } - userJoinedMessage.setUserid(clientUser.userId); - userJoinedMessage.setName(clientUser.name); - userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUserjoinedmessage(userJoinedMessage); - - emitInBatch(clientListener, subMessage); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - }, (thing: Movable, position, listener) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userMovedMessage = new UserMovedMessage(); - userMovedMessage.setUserid(clientUser.userId); - userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUsermovedmessage(userMovedMessage); - - clientListener.emitInBatch(subMessage); - //console.log("Sending USER_MOVED event"); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - }, (thing: Movable, listener) => { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - this.emitUserLeftEvent(clientListener, clientUser.userId); - } else if (thing instanceof Group) { - this.emitDeleteGroupEvent(clientListener, thing.getId()); - } else { - console.error('Unexpected type for Movable.'); - } - - }); + world = new GameRoom( + roomId, + (user: User, group: Group) => this.joinWebRtcRoom(user, group), + (user: User, group: Group) => this.disConnectedUser(user, group), + MINIMUM_DISTANCE, + GROUP_RADIUS, + (thing: Movable, listener: User) => this.onRoomEnter(thing, listener), + (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), + (thing: Movable, listener:User) => this.onClientLeave(thing, listener) + ); + if (!world.anonymous) { + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + world.tags = data.tags + world.policyType = Number(data.policy_type) + } this.Worlds.set(roomId, world); } + return Promise.resolve(world) + } + + private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { + + const roomId = client.roomId; + //join user in room + this.nbClientsPerRoomGauge.inc({ room: roomId }); + client.position = position; + + const world = this.Worlds.get(roomId) + if(world === undefined){ + throw new Error('Could not find room for ID: '+client.roomId) + } // Dispatch groups position to newly connected user world.getGroups().forEach((group: Group) => { @@ -655,6 +600,64 @@ export class IoSocketController { world.join(client, client.position); return world; } + + private onRoomEnter(thing: Movable, listener: User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userJoinedMessage = new UserJoinedMessage(); + if (!Number.isInteger(clientUser.userId)) { + throw new Error('clientUser.userId is not an integer '+clientUser.userId); + } + userJoinedMessage.setUserid(clientUser.userId); + userJoinedMessage.setName(clientUser.name); + userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUserjoinedmessage(userJoinedMessage); + + emitInBatch(clientListener, subMessage); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userMovedMessage = new UserMovedMessage(); + userMovedMessage.setUserid(clientUser.userId); + userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUsermovedmessage(userMovedMessage); + + clientListener.emitInBatch(subMessage); + //console.log("Sending USER_MOVED event"); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientLeave(thing: Movable, listener:User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + this.emitUserLeftEvent(clientListener, clientUser.userId); + } else if (thing instanceof Group) { + this.emitDeleteGroupEvent(clientListener, thing.getId()); + } else { + console.error('Unexpected type for Movable.'); + } + } private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void { const position = group.getPosition(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 1f438e61..baa54896 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -8,10 +8,18 @@ import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; import {PositionNotifier} from "./PositionNotifier"; import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; import {Movable} from "_Model/Movable"; +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; +import {arrayIntersect} from "../Services/ArrayHelper"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; +export enum GameRoomPolicyTypes { + ANONYMUS_POLICY = 1, + MEMBERS_ONLY_POLICY, + USE_TAGS_POLICY, +} + export class GameRoom { private readonly minDistance: number; private readonly groupRadius: number; @@ -26,8 +34,16 @@ export class GameRoom { private itemsState: Map = new Map(); private readonly positionNotifier: PositionNotifier; + public readonly roomId: string; + public readonly anonymous: boolean; + public tags: string[]; + public policyType: GameRoomPolicyTypes; + public readonly roomSlug: string; + public readonly worldSlug: string = ''; + public readonly organizationSlug: string = ''; - constructor(connectCallback: ConnectCallback, + constructor(roomId: string, + connectCallback: ConnectCallback, disconnectCallback: DisconnectCallback, minDistance: number, groupRadius: number, @@ -35,6 +51,21 @@ export class GameRoom { onMoves: MovesCallback, onLeaves: LeavesCallback) { + this.roomId = roomId; + this.anonymous = isRoomAnonymous(roomId); + this.tags = []; + this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; + + if (this.anonymous) { + this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); + } else { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + this.roomSlug = roomSlug; + this.organizationSlug = organizationSlug; + this.worldSlug = worldSlug; + } + + this.users = new Map(); this.groups = new Set(); this.connectCallback = connectCallback; @@ -248,4 +279,8 @@ export class GameRoom { } return this.positionNotifier.setViewport(user, viewport); } + + canAccess(userTags: string[]): boolean { + return arrayIntersect(userTags, this.tags); + } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index d1516264..3ac62bca 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,25 +1,30 @@ -export class RoomIdentifier { - public readonly anonymous: boolean; - public readonly id:string - public readonly organizationSlug: string|undefined; - public readonly worldSlug: string|undefined; - public readonly roomSlug: string|undefined; - constructor(roomID: string) { - if (roomID.startsWith('_/')) { - this.anonymous = true; - } else if(roomID.startsWith('@/')) { - this.anonymous = false; +//helper functions to parse room IDs - const match = /@\/([^/]+)\/([^/]+)\/(.+)/.exec(roomID); - if (!match) { - throw new Error('Could not extract info from "'+roomID+'"'); - } - this.organizationSlug = match[1]; - this.worldSlug = match[2]; - this.roomSlug = match[3]; - } else { - throw new Error('Incorrect room ID: '+roomID); - } - this.id = roomID; +export const isRoomAnonymous = (roomID: string): boolean => { + if (roomID.startsWith('_/')) { + return true; + } else if(roomID.startsWith('@/')) { + return false; + } else { + throw new Error('Incorrect room ID: '+roomID); } } + +export const extractRoomSlugPublicRoomId = (roomId: string): string => { + const idParts = roomId.split('/'); + if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); + return idParts.slice(2).join('/'); +} +export interface extractDataFromPrivateRoomIdResponse { + organizationSlug: string; + worldSlug: string; + roomSlug: string; +} +export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { + const idParts = roomId.split('/'); + if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const organizationSlug = idParts[1]; + const worldSlug = idParts[2]; + const roomSlug = idParts[3]; + return {organizationSlug, worldSlug, roomSlug} +} \ No newline at end of file diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 605af738..739997fd 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -1,12 +1,13 @@ import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import {RoomIdentifier} from "../Model/RoomIdentifier"; export interface AdminApiData { organizationSlug: string worldSlug: string roomSlug: string mapUrlStart: string + tags: string[] + policy_type: number userUuid: string } @@ -15,6 +16,11 @@ export interface GrantedApiData { memberTags: string[] } +export interface fetchMemberDataByUuidResponse { + uuid: string; + tags: string[]; +} + class AdminApi { async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { @@ -40,6 +46,16 @@ class AdminApi { return res.data; } + async fetchMemberDataByUuid(uuid: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('No admin backoffice set!'); + } + const res = await Axios.get(ADMIN_API_URL+'/membership/'+uuid, + { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } + ) + return res.data; + } + async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); @@ -50,24 +66,6 @@ class AdminApi { ) return res.data; } - - async memberIsGrantedAccessToRoom(memberId: string, roomIdentifier: RoomIdentifier): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - try { - const res = await Axios.get(ADMIN_API_URL+'/api/member/is-granted-access', - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, params: {memberId, organizationSlug: roomIdentifier.organizationSlug, worldSlug: roomIdentifier.worldSlug, roomSlug: roomIdentifier.roomSlug} } - ) - return res.data; - } catch (e) { - console.log(e.message) - return { - granted: false, - memberTags: [] - }; - } - } } export const adminApi = new AdminApi(); diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts new file mode 100644 index 00000000..67321d1b --- /dev/null +++ b/back/src/Services/ArrayHelper.ts @@ -0,0 +1,3 @@ +export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { + return array1.filter(value => array2.includes(value)).length > 0; +} \ No newline at end of file diff --git a/back/tests/ArrayHelperTest.ts b/back/tests/ArrayHelperTest.ts new file mode 100644 index 00000000..51796682 --- /dev/null +++ b/back/tests/ArrayHelperTest.ts @@ -0,0 +1,14 @@ +import {arrayIntersect} from "../src/Services/ArrayHelper"; + + +describe("RoomIdentifier", () => { + it("should return true on intersect", () => { + expect(arrayIntersect(['admin', 'user'], ['admin', 'superAdmin'])).toBe(true); + }); + it("should be reflexive", () => { + expect(arrayIntersect(['admin', 'superAdmin'], ['admin', 'user'])).toBe(true); + }); + it("should return false on non intersect", () => { + expect(arrayIntersect(['admin', 'user'], ['superAdmin'])).toBe(false); + }); +}) \ No newline at end of file diff --git a/back/tests/RoomIdentifierTest.ts b/back/tests/RoomIdentifierTest.ts new file mode 100644 index 00000000..c3817ff7 --- /dev/null +++ b/back/tests/RoomIdentifierTest.ts @@ -0,0 +1,19 @@ +import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier"; + +describe("RoomIdentifier", () => { + it("should flag public id as anonymous", () => { + expect(isRoomAnonymous('_/global/test')).toBe(true); + }); + it("should flag public id as not anonymous", () => { + expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false); + }); + it("should extract roomSlug from public ID", () => { + expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json'); + }); + it("should extract correct from private ID", () => { + const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor'); + expect(organizationSlug).toBe('afup'); + expect(worldSlug).toBe('afup2020'); + expect(roomSlug).toBe('1floor'); + }); +}) \ No newline at end of file diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 5e06414c..a4161bf5 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -21,7 +21,7 @@ describe("World", () => { } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -48,7 +48,7 @@ describe("World", () => { } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100)); @@ -77,7 +77,7 @@ describe("World", () => { disconnectCallNumber++; } - const world = new GameRoom(connect, disconnect, 160, 160, () => {}, () => {}, () => {}); + const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}); world.join(createMockUser(1), new Point(100, 100));