Removing all "any" from back.
To do this, I used generic-type-guard package which generates both an interface AND a valid type guard from code. With this, we are 100% sure that the messages we receive are validated at runtime! The client cannot pass us an object that is invalid! \o/
This commit is contained in:
parent
ac0b7a7361
commit
a373626e24
@ -16,7 +16,8 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thecodingmachine/workadventure.git"
|
||||
},
|
||||
"contributors": [{
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Grégoire Parant",
|
||||
"email": "g.parant@thecodingmachine.com"
|
||||
},
|
||||
@ -27,7 +28,8 @@
|
||||
{
|
||||
"name": "Arthmaël Poly",
|
||||
"email": "a.poly@thecodingmachine.com"
|
||||
}],
|
||||
}
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"bugs": {
|
||||
"url": "https://github.com/thecodingmachine/workadventure/issues"
|
||||
@ -41,6 +43,7 @@
|
||||
"@types/uuidv4": "^5.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"generic-type-guard": "^3.2.0",
|
||||
"http-status-codes": "^1.4.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"prom-client": "^12.0.0",
|
||||
|
@ -4,6 +4,11 @@ import {BAD_REQUEST, OK} from "http-status-codes";
|
||||
import {SECRET_KEY, URL_ROOM_STARTED} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
|
||||
import { uuid } from 'uuidv4';
|
||||
|
||||
export interface TokenInterface {
|
||||
name: string,
|
||||
userId: string
|
||||
}
|
||||
|
||||
export class AuthenticateController {
|
||||
App : Application;
|
||||
|
||||
@ -24,7 +29,7 @@ export class AuthenticateController {
|
||||
}*/
|
||||
//TODO check user email for The Coding Machine game
|
||||
const userId = uuid();
|
||||
const token = Jwt.sign({name: param.name, userId: userId}, SECRET_KEY, {expiresIn: '24h'});
|
||||
const token = Jwt.sign({name: param.name, userId: userId} as TokenInterface, SECRET_KEY, {expiresIn: '24h'});
|
||||
return res.status(OK).send({
|
||||
token: token,
|
||||
mapUrlStart: URL_ROOM_STARTED,
|
||||
|
@ -8,12 +8,17 @@ import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVar
|
||||
import {World} from "../Model/World";
|
||||
import {Group} from "_Model/Group";
|
||||
import {UserInterface} from "_Model/UserInterface";
|
||||
import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage";
|
||||
import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage";
|
||||
import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
|
||||
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
|
||||
import si from "systeminformation";
|
||||
import {Gauge} from "prom-client";
|
||||
import os from 'os';
|
||||
import {TokenInterface} from "../Controller/AuthenticateController";
|
||||
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
|
||||
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
|
||||
import {isWebRtcSignalMessageInterface} from "../Model/Websocket/WebRtcSignalMessage";
|
||||
import {UserInGroupInterface} from "../Model/Websocket/UserInGroupInterface";
|
||||
|
||||
enum SockerIoEvent {
|
||||
CONNECTION = "connection",
|
||||
@ -23,7 +28,6 @@ enum SockerIoEvent {
|
||||
USER_MOVED = "user-moved", // From server to client
|
||||
USER_LEFT = "user-left", // From server to client
|
||||
WEBRTC_SIGNAL = "webrtc-signal",
|
||||
WEBRTC_OFFER = "webrtc-offer",
|
||||
WEBRTC_START = "webrtc-start",
|
||||
WEBRTC_DISCONNECT = "webrtc-disconect",
|
||||
MESSAGE_ERROR = "message-error",
|
||||
@ -61,10 +65,15 @@ export class IoSocketController {
|
||||
if(this.searchClientByToken(socket.handshake.query.token)){
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: any) => {
|
||||
Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
|
||||
if (err) {
|
||||
return next(new Error('Authentication error'));
|
||||
}
|
||||
|
||||
if (!this.isValidToken(tokenDecoded)) {
|
||||
return next(new Error('Authentication error, invalid token structure'));
|
||||
}
|
||||
|
||||
(socket as ExSocketInterface).token = tokenDecoded;
|
||||
(socket as ExSocketInterface).userId = tokenDecoded.userId;
|
||||
next();
|
||||
@ -74,14 +83,24 @@ export class IoSocketController {
|
||||
this.ioConnection();
|
||||
}
|
||||
|
||||
private isValidToken(token: object): token is TokenInterface {
|
||||
if (typeof((token as TokenInterface).userId) !== 'string') {
|
||||
return false;
|
||||
}
|
||||
if (typeof((token as TokenInterface).name) !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param token
|
||||
*/
|
||||
searchClientByToken(token: string): ExSocketInterface | null {
|
||||
const clients: Array<any> = Object.values(this.Io.sockets.sockets);
|
||||
const clients: ExSocketInterface[] = Object.values(this.Io.sockets.sockets) as ExSocketInterface[];
|
||||
for (let i = 0; i < clients.length; i++) {
|
||||
const client: ExSocketInterface = clients[i];
|
||||
const client = clients[i];
|
||||
if (client.token !== token) {
|
||||
continue
|
||||
}
|
||||
@ -131,20 +150,15 @@ export class IoSocketController {
|
||||
x: user x position on map
|
||||
y: user y position on map
|
||||
*/
|
||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: any, answerFn): void => {
|
||||
socket.on(SockerIoEvent.JOIN_ROOM, (message: unknown, answerFn): void => {
|
||||
try {
|
||||
if (!isJoinRoomMessageInterface(message)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid JOIN_ROOM message.'});
|
||||
console.warn('Invalid JOIN_ROOM message received: ', message);
|
||||
return;
|
||||
}
|
||||
const roomId = message.roomId;
|
||||
|
||||
if (typeof(roomId) !== 'string') {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Expected roomId as a string.'});
|
||||
return;
|
||||
}
|
||||
const position = this.hydratePositionReceive(message.position);
|
||||
if (position instanceof Error) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
|
||||
return;
|
||||
}
|
||||
|
||||
const Client = (socket as ExSocketInterface);
|
||||
|
||||
if (Client.roomId === roomId) {
|
||||
@ -155,7 +169,7 @@ export class IoSocketController {
|
||||
this.leaveRoom(Client);
|
||||
|
||||
//join new previous room
|
||||
const world = this.joinRoom(Client, roomId, position);
|
||||
const world = this.joinRoom(Client, roomId, message.position);
|
||||
|
||||
//add function to refresh position user in real time.
|
||||
//this.refreshUserPosition(Client);
|
||||
@ -176,11 +190,11 @@ export class IoSocketController {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.USER_POSITION, (message: any): void => {
|
||||
socket.on(SockerIoEvent.USER_POSITION, (position: unknown): void => {
|
||||
try {
|
||||
const position = this.hydratePositionReceive(message);
|
||||
if (position instanceof Error) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
|
||||
if (!isPointInterface(position)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid USER_POSITION message.'});
|
||||
console.warn('Invalid USER_POSITION message received: ', position);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -204,7 +218,12 @@ export class IoSocketController {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: any) => {
|
||||
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
|
||||
if (!isWebRtcSignalMessageInterface(data)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'});
|
||||
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
|
||||
return;
|
||||
}
|
||||
//send only at user
|
||||
const client = this.sockets.get(data.receiverId);
|
||||
if (client === undefined) {
|
||||
@ -214,16 +233,6 @@ export class IoSocketController {
|
||||
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.WEBRTC_OFFER, (data: any) => {
|
||||
//send only at user
|
||||
const client = this.sockets.get(data.receiverId);
|
||||
if (client === undefined) {
|
||||
console.warn("While exchanging a WebRTC offer: client with id ", data.receiverId, " does not exist. This might be a race condition.");
|
||||
return;
|
||||
}
|
||||
client.emit(SockerIoEvent.WEBRTC_OFFER, data);
|
||||
});
|
||||
|
||||
socket.on(SockerIoEvent.DISCONNECT, () => {
|
||||
const Client = (socket as ExSocketInterface);
|
||||
try {
|
||||
@ -254,7 +263,12 @@ export class IoSocketController {
|
||||
});
|
||||
|
||||
// Let's send the user id to the user
|
||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: SetPlayerDetailsMessage, answerFn) => {
|
||||
socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: unknown, answerFn) => {
|
||||
if (!isSetPlayerDetailsMessage(playerDetails)) {
|
||||
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid SET_PLAYER_DETAILS message.'});
|
||||
console.warn('Invalid SET_PLAYER_DETAILS message received: ', playerDetails);
|
||||
return;
|
||||
}
|
||||
const Client = (socket as ExSocketInterface);
|
||||
Client.name = playerDetails.name;
|
||||
Client.character = playerDetails.character;
|
||||
@ -288,7 +302,7 @@ export class IoSocketController {
|
||||
}
|
||||
}
|
||||
|
||||
private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World {
|
||||
private joinRoom(Client : ExSocketInterface, roomId: string, position: PointInterface): World {
|
||||
//join user in room
|
||||
Client.join(roomId);
|
||||
this.nbClientsPerRoomGauge.inc({ host: os.hostname(), room: roomId });
|
||||
@ -343,7 +357,7 @@ export class IoSocketController {
|
||||
//send all users in room to create PeerConnection in front
|
||||
clients.forEach((client: ExSocketInterface, index: number) => {
|
||||
|
||||
const clientsId = clients.reduce((tabs: Array<any>, clientId: ExSocketInterface, indexClientId: number) => {
|
||||
const clientsId = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
|
||||
if (!clientId.userId || clientId.userId === client.userId) {
|
||||
return tabs;
|
||||
}
|
||||
@ -359,19 +373,6 @@ export class IoSocketController {
|
||||
});
|
||||
}
|
||||
|
||||
//Hydrate and manage error
|
||||
hydratePositionReceive(message: any): Point | Error {
|
||||
try {
|
||||
if (!message.x || !message.y || !message.direction || message.moving === undefined) {
|
||||
return new Error("invalid point message sent");
|
||||
}
|
||||
return new Point(message.x, message.y, message.direction, message.moving);
|
||||
} 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 :
|
||||
|
@ -1,9 +1,10 @@
|
||||
import {Socket} from "socket.io";
|
||||
import {PointInterface} from "./PointInterface";
|
||||
import {Identificable} from "./Identificable";
|
||||
import {TokenInterface} from "../../Controller/AuthenticateController";
|
||||
|
||||
export interface ExSocketInterface extends Socket, Identificable {
|
||||
token: any;
|
||||
token: TokenInterface;
|
||||
roomId: string;
|
||||
webRtcRoomId: string;
|
||||
userId: string;
|
||||
|
9
back/src/Model/Websocket/JoinRoomMessage.ts
Normal file
9
back/src/Model/Websocket/JoinRoomMessage.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
import {isPointInterface} from "./PointInterface";
|
||||
|
||||
export const isJoinRoomMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
roomId: tg.isString,
|
||||
position: isPointInterface,
|
||||
}).get();
|
||||
export type JoinRoomMessageInterface = tg.GuardedType<typeof isJoinRoomMessageInterface>;
|
@ -1,4 +1,3 @@
|
||||
import {Point} from "./MessageUserPosition";
|
||||
import {PointInterface} from "_Model/Websocket/PointInterface";
|
||||
|
||||
export class MessageUserJoined {
|
||||
|
@ -1,5 +1,17 @@
|
||||
export interface PointInterface {
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
/*export interface PointInterface {
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly direction: string;
|
||||
}
|
||||
readonly moving: boolean;
|
||||
}*/
|
||||
|
||||
export const isPointInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
x: tg.isNumber,
|
||||
y: tg.isNumber,
|
||||
direction: tg.isString,
|
||||
moving: tg.isBoolean
|
||||
}).get();
|
||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||
|
@ -1,4 +1,8 @@
|
||||
export interface SetPlayerDetailsMessage {
|
||||
name: string,
|
||||
character: string
|
||||
}
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isSetPlayerDetailsMessage =
|
||||
new tg.IsInterface().withProperties({
|
||||
name: tg.isString,
|
||||
character: tg.isString
|
||||
}).get();
|
||||
export type SetPlayerDetailsMessage = tg.GuardedType<typeof isSetPlayerDetailsMessage>;
|
||||
|
5
back/src/Model/Websocket/UserInGroupInterface.ts
Normal file
5
back/src/Model/Websocket/UserInGroupInterface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface UserInGroupInterface {
|
||||
userId: string,
|
||||
name: string,
|
||||
initiator: boolean
|
||||
}
|
10
back/src/Model/Websocket/WebRtcSignalMessage.ts
Normal file
10
back/src/Model/Websocket/WebRtcSignalMessage.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isWebRtcSignalMessageInterface =
|
||||
new tg.IsInterface().withProperties({
|
||||
userId: tg.isString,
|
||||
receiverId: tg.isString,
|
||||
roomId: tg.isString,
|
||||
signal: tg.isUnknown
|
||||
}).get();
|
||||
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;
|
@ -790,6 +790,11 @@ functional-red-black-tree@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
|
||||
generic-type-guard@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/generic-type-guard/-/generic-type-guard-3.2.0.tgz#1fb136f934730c776486526b8a21fe96b067e691"
|
||||
integrity sha512-EkkrXYbOtJ3VPB+SOrU7EhwY65rZErItGtBg5wAqywaj07BOubwOZqMYaxOWekJ9akioGqXIsw1fYk3wwbWsDQ==
|
||||
|
||||
get-stdin@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||
|
Loading…
Reference in New Issue
Block a user