From 5de2f6123179739b637efa2ecfd6d37eac95e26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Wed, 30 Sep 2020 10:12:40 +0200 Subject: [PATCH] Adding back authentication to uws websocket --- back/src/Controller/AdminController.ts | 0 back/src/Controller/AuthenticateController.ts | 5 +- back/src/Controller/IoSocketController.ts | 134 +++++++++++++----- back/src/Model/Websocket/ExSocketInterface.ts | 1 - 4 files changed, 98 insertions(+), 42 deletions(-) delete mode 100644 back/src/Controller/AdminController.ts diff --git a/back/src/Controller/AdminController.ts b/back/src/Controller/AdminController.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/back/src/Controller/AuthenticateController.ts b/back/src/Controller/AuthenticateController.ts index d8d92cc1..ce97eb0e 100644 --- a/back/src/Controller/AuthenticateController.ts +++ b/back/src/Controller/AuthenticateController.ts @@ -39,6 +39,7 @@ export class AuthenticateController extends BaseController { res.onAborted(() => { console.warn('Login request was aborted'); }) + const host = req.getHeader('host'); const param = await res.json(); //todo: what to do if the organizationMemberToken is already used? @@ -63,7 +64,7 @@ export class AuthenticateController extends BaseController { newUrl = this.getNewUrlOnAdminAuth(data) } else { userUuid = uuid(); - mapUrlStart = req.getHeader('host').replace('api.', 'maps.') + URL_ROOM_STARTED; + mapUrlStart = host.replace('api.', 'maps.') + URL_ROOM_STARTED; newUrl = null; } @@ -76,7 +77,7 @@ export class AuthenticateController extends BaseController { })); } catch (e) { - console.log(e.message) + console.log("An error happened", e) res.writeStatus(e.status || "500 Internal Server Error").end('An error happened'); } diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index c8753371..49ff7f49 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -47,7 +47,8 @@ import { import {UserMovesMessage} from "../Messages/generated/messages_pb"; import Direction = PositionMessage.Direction; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {App, TemplatedApp, WebSocket} from "uWebSockets.js" +import {App, HttpRequest, TemplatedApp, WebSocket} from "uWebSockets.js" +import {parse} from "query-string"; enum SocketIoEvent { CONNECTION = "connection", @@ -135,67 +136,122 @@ export class IoSocketController { return null; }*/ - private authenticate(ws: WebSocket) { + private async authenticate(req: HttpRequest): Promise<{ token: string, userUuid: string }> { //console.log(socket.handshake.query.token); - /*if (!socket.handshake.query || !socket.handshake.query.token) { - console.error('An authentication error happened, a user tried to connect without a token.'); - return next(new Error('Authentication error')); - } - if(socket.handshake.query.token === 'test'){ - if (ALLOW_ARTILLERY) { - (socket as ExSocketInterface).token = socket.handshake.query.token; - (socket as ExSocketInterface).userId = this.nextUserId; - (socket as ExSocketInterface).userUuid = uuid(); - this.nextUserId++; - (socket as ExSocketInterface).isArtillery = true; - console.log((socket as ExSocketInterface).userId); - next(); - return; - } else { - console.warn("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); - next(); + const query = parse(req.getQuery()); + + if (!query.token) { + console.error('An authentication error happened, a user tried to connect without a 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'); + } + + + if(token === 'test'){ + if (ALLOW_ARTILLERY) { + return { + token, + userUuid: 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'"); } - (socket as ExSocketInterface).isArtillery = false; - 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')); - } - Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => { + } + + /*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) => { + Jwt.verify(token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => { const tokenInterface = tokenDecoded as TokenInterface; if (err) { console.error('An authentication error happened, invalid JsonWebToken.', err); - return next(new Error('Authentication error')); + reject(new Error('An authentication error happened, invalid JsonWebToken. '+err.message)); + return; } if (!this.isValidToken(tokenInterface)) { - return next(new Error('Authentication error, invalid token structure')); + reject(new Error('Authentication error, invalid token structure.')); + return; } - (socket as ExSocketInterface).token = socket.handshake.query.token; - (socket as ExSocketInterface).userId = this.nextUserId; - (socket as ExSocketInterface).userUuid = tokenInterface.userUuid; - this.nextUserId++; - next(); - });*/ - const socket = ws as ExSocketInterface; - socket.userId = this.nextUserId; - this.nextUserId++; + resolve({ + token, + userUuid: tokenInterface.userUuid + }); + }); + }); + + return promise; } ioConnection() { this.app.ws('/*', { + /* Options */ //compression: uWS.SHARED_COMPRESSOR, maxPayloadLength: 16 * 1024 * 1024, //idleTimeout: 10, + 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}; + + res.onAborted(() => { + /* We can simply signal that we were aborted */ + upgradeAborted.aborted = true; + }); + + try { + const result = await this.authenticate(req); + + if (upgradeAborted.aborted) { + console.log("Ouch! Client disconnected before we could upgrade it!"); + /* You must not upgrade now */ + return; + } + + /* 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(), + token: result.token, + userUuid: result.userUuid + }, + /* Spell these correctly */ + req.getHeader('sec-websocket-key'), + req.getHeader('sec-websocket-protocol'), + req.getHeader('sec-websocket-extensions'), + context); + + } catch (e: unknown) { + if (e instanceof Error) { + console.warn(e.message); + res.writeStatus("401 Unauthorized").end(e.message); + } else { + console.warn(e); + res.writeStatus("500 Internal Server Error").end('An error occurred'); + } + return; + } + })(); + }, /* Handlers */ open: (ws) => { - this.authenticate(ws); - // TODO: close if authenticate is ko - const client : ExSocketInterface = ws as ExSocketInterface; + client.userId = this.nextUserId; + this.nextUserId++; + client.userUuid = ws.userUuid; + client.token = ws.token; client.batchedMessages = new BatchMessage(); client.batchTimeout = null; client.emitInBatch = (payload: SubMessage): void => { diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index 00aeadb7..d70205ef 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -13,7 +13,6 @@ export interface ExSocketInterface extends WebSocket, Identificable { characterLayers: string[]; position: PointInterface; viewport: ViewportInterface; - isArtillery: boolean; // Whether this socket is opened by Artillery for load testing (hack) /** * Pushes an event that will be sent in the next batch of events */