workadventure/back/src/Controller/IoSocketController.ts
David Négrier 4de552437d Completely getting rid of "userid"
Previously, userid was generated by the "/login" route and passed along.
This commit completely removes the uuid "userid" (and disables the LoginController too and any Jwt check).

"userid" is replaced by the "socket id" of the connection.
So a user is now identified using a socket id, which is unique for a given connection.
2020-05-14 23:20:43 +02:00

449 lines
16 KiB
TypeScript

import socketIO = require('socket.io');
import {Socket} from "socket.io";
import * as http from "http";
import {MessageUserPosition} 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 {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRooms";
import {ExtRoomsInterface} from "../Model/Websocket/ExtRoomsInterface";
import {World} from "../Model/World";
import {Group} from "_Model/Group";
import {UserInterface} from "_Model/UserInterface";
enum SockerIoEvent {
CONNECTION = "connection",
DISCONNECT = "disconnect",
ATTRIBUTE_USER_ID = "attribute-user-id", // Sent from server to client just after the connexion is established to give the client its unique id.
JOIN_ROOM = "join-room",
USER_POSITION = "user-position",
WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_OFFER = "webrtc-offer",
WEBRTC_START = "webrtc-start",
WEBRTC_DISCONNECT = "webrtc-disconect",
MESSAGE_ERROR = "message-error",
GROUP_CREATE_UPDATE = "group-create-update",
GROUP_DELETE = "group-delete",
}
export class IoSocketController {
Io: socketIO.Server;
Worlds: Map<string, World> = new Map<string, World>();
constructor(server: http.Server) {
this.Io = socketIO(server);
// Authentication with token. it will be decoded and stored in the socket.
// Completely commented for now, as we do not use the "/login" route at all.
/*this.Io.use((socket: Socket, next) => {
if (!socket.handshake.query || !socket.handshake.query.token) {
return next(new Error('Authentication error'));
}
if(this.searchClientByToken(socket.handshake.query.token)){
return next(new Error('Authentication error'));
}
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
if (err) {
return next(new Error('Authentication error'));
}
(socket as ExSocketInterface).token = tokenDecoded;
next();
});
});*/
this.ioConnection();
this.shareUsersPosition();
}
private sendUpdateGroupEvent(group: Group): void {
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
// Note: this is suboptimal
let userId = group.getUsers()[0].id;
let client: ExSocketInterface|null = this.searchClientById(userId);
if (client === null) {
return;
}
let roomId = client.roomId;
this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
position: group.getPosition(),
groupId: group.getId()
});
}
private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void {
// Let's get the room of the group. To do this, let's get anyone in the group and find its room.
// Note: this is suboptimal
let userId = lastUser.id;
let client: ExSocketInterface|null = this.searchClientById(userId);
if (client === null) {
console.warn('Could not find client ', userId, ' in group')
return;
}
let roomId = client.roomId;
this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
}
ioConnection() {
this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
/*join-rom event permit to join one room.
message :
userId : user identification
roomId: room identification
position: position of user in map
x: user x position on map
y: user y position on map
*/
socket.on(SockerIoEvent.JOIN_ROOM, (message: string) => {
try {
let messageUserPosition = this.hydrateMessageReceive(message);
if (messageUserPosition instanceof Error) {
return socket.emit(SockerIoEvent.MESSAGE_ERROR, JSON.stringify({message: messageUserPosition.message}))
}
let Client = (socket as ExSocketInterface);
if (Client.roomId === messageUserPosition.roomId) {
return;
}
//leave previous room
this.leaveRoom(Client);
//join new previous room
this.joinRoom(Client, messageUserPosition);
// sending to all clients in room except sender
this.saveUserInformation(Client, messageUserPosition);
//add function to refresh position user in real time.
this.refreshUserPosition(Client);
socket.to(messageUserPosition.roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserPosition.toString());
} catch (e) {
console.error('An error occurred on "join_room" event');
console.error(e);
}
});
socket.on(SockerIoEvent.USER_POSITION, (message: string) => {
try {
let messageUserPosition = this.hydrateMessageReceive(message);
if (messageUserPosition instanceof Error) {
return socket.emit(SockerIoEvent.MESSAGE_ERROR, JSON.stringify({message: messageUserPosition.message}));
}
let Client = (socket as ExSocketInterface);
// sending to all clients in room except sender
this.saveUserInformation(Client, messageUserPosition);
//refresh position of all user in all rooms in real time
this.refreshUserPosition(Client);
} catch (e) {
console.error('An error occurred on "user_position" event');
console.error(e);
}
});
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (message: string) => {
let data: any = JSON.parse(message);
//send only at user
let client = this.searchClientById(data.receiverId);
if (!client) {
console.error("client doesn't exist for ", data.receiverId);
return;
}
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, message);
});
socket.on(SockerIoEvent.WEBRTC_OFFER, (message: string) => {
let data: any = JSON.parse(message);
//send only at user
let client = this.searchClientById(data.receiverId);
if (!client) {
console.error("client doesn't exist for ", data.receiverId);
return;
}
client.emit(SockerIoEvent.WEBRTC_OFFER, message);
});
socket.on(SockerIoEvent.DISCONNECT, () => {
try {
let Client = (socket as ExSocketInterface);
this.sendDisconnectedEvent(Client);
//refresh position of all user in all rooms in real time
this.refreshUserPosition(Client);
//leave room
this.leaveRoom(Client);
//leave webrtc room
socket.leave(Client.webRtcRoomId);
//delete all socket information
delete Client.webRtcRoomId;
delete Client.roomId;
delete Client.token;
delete Client.position;
} catch (e) {
console.error('An error occurred on "disconnect"');
console.error(e);
}
});
// Let's send the user id to the user
socket.emit(SockerIoEvent.ATTRIBUTE_USER_ID, socket.id);
});
}
/**
* TODO: each call to this method is suboptimal. It means that instead of passing an ID, we should pass a client object.
* @param userId
*/
searchClientById(userId: string): ExSocketInterface | null {
let clients: Array<any> = Object.values(this.Io.sockets.sockets);
for (let i = 0; i < clients.length; i++) {
let client: ExSocketInterface = clients[i];
if (client.id !== userId) {
continue;
}
return client;
}
console.log("Could not find user with id ", userId);
return null;
}
/**
* @param userId
*/
searchClientByToken(userId: string): ExSocketInterface | null {
let clients: Array<any> = Object.values(this.Io.sockets.sockets);
for (let i = 0; i < clients.length; i++) {
let client: ExSocketInterface = clients[i];
if (client.id !== userId) {
continue
}
return client;
}
return null;
}
/**
*
* @param Client: ExSocketInterface
*/
sendDisconnectedEvent(Client: ExSocketInterface) {
Client.broadcast.emit(SockerIoEvent.WEBRTC_DISCONNECT, JSON.stringify({
userId: Client.id
}));
//disconnect webrtc room
if(!Client.webRtcRoomId){
return;
}
Client.leave(Client.webRtcRoomId);
delete Client.webRtcRoomId;
}
/**
*
* @param Client
*/
leaveRoom(Client : ExSocketInterface){
//lease previous room and world
if(Client.roomId){
//user leave previous world
let world : World|undefined = this.Worlds.get(Client.roomId);
if(world){
console.log('Entering world.leave')
world.leave(Client);
//this.Worlds.set(Client.roomId, world);
}
//user leave previous room
Client.leave(Client.roomId);
delete Client.roomId;
}
}
/**
*
* @param Client
* @param messageUserPosition
*/
joinRoom(Client : ExSocketInterface, messageUserPosition: MessageUserPosition){
//join user in room
Client.join(messageUserPosition.roomId);
//check and create new world for a room
if(!this.Worlds.get(messageUserPosition.roomId)){
let world = new World((user1: string, group: Group) => {
this.connectedUser(user1, group);
}, (user1: string, group: Group) => {
this.disConnectedUser(user1, group);
}, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => {
this.sendUpdateGroupEvent(group);
}, (groupUuid: string, lastUser: UserInterface) => {
this.sendDeleteGroupEvent(groupUuid, lastUser);
});
this.Worlds.set(messageUserPosition.roomId, world);
}
let world : World|undefined = this.Worlds.get(messageUserPosition.roomId);
if(world) {
// Dispatch groups position to newly connected user
world.getGroups().forEach((group: Group) => {
Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
position: group.getPosition(),
groupId: group.getId()
});
});
//join world
world.join(Client, messageUserPosition);
this.Worlds.set(messageUserPosition.roomId, world);
}
}
/**
*
* @param socket
* @param roomId
*/
joinWebRtcRoom(socket: ExSocketInterface, roomId: string) {
if (socket.webRtcRoomId === roomId) {
return;
}
socket.join(roomId);
socket.webRtcRoomId = roomId;
//if two persons in room share
if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
return;
}
let clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
//send start at one client to initialise offer webrtc
//send all users in room to create PeerConnection in front
clients.forEach((client: ExSocketInterface, index: number) => {
let clientsId = clients.reduce((tabs: Array<any>, clientId: ExSocketInterface, indexClientId: number) => {
if (!clientId.id || clientId.id === client.id) {
return tabs;
}
tabs.push({
userId: clientId.id,
name: clientId.name,
initiator: index <= indexClientId
});
return tabs;
}, []);
client.emit(SockerIoEvent.WEBRTC_START, JSON.stringify({clients: clientsId, roomId: roomId}));
});
}
//permit to save user position in socket
saveUserInformation(socket: ExSocketInterface, message: MessageUserPosition) {
socket.position = message.position;
socket.roomId = message.roomId;
//socket.userId = message.userId;
socket.name = message.name;
socket.character = message.character;
}
refreshUserPosition(Client : ExSocketInterface) {
//refresh position of all user in all rooms in real time
let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface);
if (!rooms.refreshUserPosition) {
rooms.refreshUserPosition = RefreshUserPositionFunction;
}
rooms.refreshUserPosition(rooms, this.Io);
// update position in the world
let data = {
userId: Client.id,
roomId: Client.roomId,
position: Client.position,
name: Client.name,
character: Client.character,
};
let messageUserPosition = new MessageUserPosition(data);
let world = this.Worlds.get(messageUserPosition.roomId);
if (!world) {
return;
}
world.updatePosition(Client, messageUserPosition);
this.Worlds.set(messageUserPosition.roomId, world);
}
//Hydrate and manage error
hydrateMessageReceive(message: string): MessageUserPosition | Error {
try {
return new MessageUserPosition(JSON.parse(message));
} catch (err) {
//TODO log error
return new Error(err);
}
}
/** permit to share user position
** users position will send in event 'user-position'
** The data sent is an array with information for each user :
[
{
userId: <string>,
roomId: <string>,
position: {
x : <number>,
y : <number>,
direction: <string>
}
},
...
]
**/
seTimeOutInProgress: any = null;
shareUsersPosition() {
if (this.seTimeOutInProgress) {
clearTimeout(this.seTimeOutInProgress);
}
//send for each room, all data of position user
let arrayMap = (this.Io.sockets.adapter.rooms as ExtRooms).userPositionMapByRoom;
if (!arrayMap) {
this.seTimeOutInProgress = setTimeout(() => {
this.shareUsersPosition();
}, 10);
return;
}
arrayMap.forEach((value: any) => {
let roomId = value[0];
this.Io.in(roomId).emit(SockerIoEvent.USER_POSITION, JSON.stringify(value));
});
this.seTimeOutInProgress = setTimeout(() => {
this.shareUsersPosition();
}, 10);
}
//connected user
connectedUser(userId: string, group: Group) {
let Client = this.searchClientById(userId);
if (!Client) {
return;
}
this.joinWebRtcRoom(Client, group.getId());
}
//disconnect user
disConnectedUser(userId: string, group: Group) {
let Client = this.searchClientById(userId);
if (!Client) {
return;
}
this.sendDisconnectedEvent(Client)
}
}