workadventure/pusher/src/Controller/IoSocketController.ts
Gregoire Parant c177f0a1b3 Squashed commit of the following:
commit 41748a4036
Merge: 3b1d4d63 4991a70b
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:38:37 2021 +0200

    Merge pull request #1327 from thecodingmachine/hotFixErrorCardBack

    Fix error generated

commit 4991a70bba
Author: Gregoire Parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:34:03 2021 +0200

    Fix error generated

    Don't generate error if file is Invalid

commit 3b1d4d630c
Merge: f52b4598 02e5860e
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 21:03:18 2021 +0200

    Merge pull request #1326 from thecodingmachine/HotFixCreateMapFeature

    Hot fix create map feature

commit 02e5860e43
Author: Gregoire Parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 20:59:13 2021 +0200

    HotFix redirect on production domain of WorkAdventure

     - Update domain `ADMIN_URL` by `workadventu.re`

commit f52b459872
Merge: 3d657b4a 3ab069d6
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Mon Aug 2 11:23:16 2021 +0200

    Merge pull request #1324 from thecodingmachine/develop

    Release v1.4.11

commit 3ab069d650
Merge: 2b748138 9d4ffe54
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 15:51:07 2021 +0200

    Merge pull request #1323 from thecodingmachine/openIDPoc

    FIX: bomp the node version of pusher

commit 9d4ffe542c
Author: kharhamel <oognic@gmail.com>
Date:   Fri Jul 30 15:50:30 2021 +0200

    FIX: bomp the node version of pusher

commit 2b7481383f
Merge: 74975ac9 9c803a69
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 15:48:56 2021 +0200

    Merge pull request #1251 from thecodingmachine/openIDPoc

    POC for the openID connect

commit 9c803a69ff
Author: kharhamel <oognic@gmail.com>
Date:   Tue Jul 27 16:37:01 2021 +0200

    FEATURE: users can now login via an openID client

commit 74975ac9d8
Merge: 315fe7ca ebdcf880
Author: Kharhamel <Kharhamel@users.noreply.github.com>
Date:   Fri Jul 30 14:54:33 2021 +0200

    Merge pull request #1322 from thecodingmachine/improveCapacityWarning

    FEATURE: improved the room capacity warning visuals

commit ebdcf8804d
Author: kharhamel <oognic@gmail.com>
Date:   Fri Jul 30 14:08:27 2021 +0200

    added admin link to the warning container

commit 41ac51f291
Author: kharhamel <oognic@gmail.com>
Date:   Thu Jul 29 18:02:36 2021 +0200

    FEATURE: improved the room capacity warning visuals

commit 315fe7ca82
Author: David Négrier <d.negrier@thecodingmachine.com>
Date:   Thu Jul 29 17:49:51 2021 +0200

    Adding a "font-family" property for text objects. (#1311)

    - Tiled displays your system fonts.
    - Computers have different sets of fonts. Therefore, browsers never rely on system fonts
    - Which means if you select a font in Tiled, it is quite unlikely it will render properly in WorkAdventure

    To circumvent this problem, in your text object in Tiled, you can now add an additional property: `font-family`.

    The `font-family` property can contain any "web-font" that can be loaded by your browser.

    This allows us to use the "Press Start 2P" 8px font in text objects, which renders way better than the default "Sans serif" font of your browser.

commit 7ffe564e8e
Author: GRL78 <80678534+GRL78@users.noreply.github.com>
Date:   Thu Jul 29 17:42:16 2021 +0200

    Graphic upgrade of the global message console (#1287)

    * Graphic upgrade of the global message console
    Fix: error if LoginScene doesn't exist

    * Rework graphic of global message console

    * Rework graphic of global message console

    * Remove console.log

commit 2a1af2a131
Author: grégoire parant <g.parant@thecodingmachine.com>
Date:   Thu Jul 29 16:42:31 2021 +0200

    PWA service workers (#1319)

    * PWA services worker

    - [x] Register service worker of PWA to install WorkAdventure application on desktop and mobile
    - [x] Create webpage specifique for PWA
    - [ ] Add register service to save and redirect on a card
    - [ ] Add possibilities to install PWA for one World (with register token if existing)

    * Finish PWA strategy to load last map visited

    * Fix feedback @Kharhamel

    * Fix feedback @Kharhamel
2021-08-02 22:06:24 +02:00

441 lines
21 KiB
TypeScript

import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { PointInterface } from "../Model/Websocket/PointInterface";
import {
SetPlayerDetailsMessage,
SubMessage,
BatchMessage,
ItemEventMessage,
ViewportMessage,
ClientToServerMessage,
SilentMessage,
WebRtcSignalToServerMessage,
PlayGlobalMessage,
ReportPlayerMessage,
QueryJitsiJwtMessage,
SendUserMessage,
ServerToClientMessage,
CompanionMessage,
EmotePromptMessage,
VariableMessage,
} from "../Messages/generated/messages_pb";
import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string";
import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { v4 } from "uuid";
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";
export class IoSocketController {
private nextUserId: number = 1;
constructor(private readonly app: TemplatedApp) {
this.ioConnection();
this.adminRoomSocket();
}
adminRoomSocket() {
this.app.ws("/admin/rooms", {
upgrade: (res, req, context) => {
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 token = query.token;
if (token !== ADMIN_API_TOKEN) {
console.log("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
return;
}
const roomId = query.roomId;
if (typeof roomId !== "string") {
console.error("Received");
res.writeStatus("400 Bad Request").end("Missing room id");
return;
}
res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context);
},
open: (ws) => {
console.log("Admin socket connect for room: " + ws.roomId);
ws.disconnecting = false;
socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string);
},
message: (ws, arrayBuffer, isBinary): void => {
try {
const roomId = ws.roomId as string;
//TODO refactor message type and data
const message: { event: string; message: { type: string; message: unknown; userUuid: string } } =
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
if (message.event === "user-message") {
const messageToEmit = message.message as { message: string; type: string; userUuid: string };
if (messageToEmit.type === "banned") {
socketManager.emitBan(
messageToEmit.userUuid,
messageToEmit.message,
messageToEmit.type,
ws.roomId as string
);
}
if (messageToEmit.type === "ban") {
socketManager.emitSendUserMessage(
messageToEmit.userUuid,
messageToEmit.message,
messageToEmit.type,
ws.roomId as string
);
}
}
} catch (err) {
console.error(err);
}
},
close: (ws, code, message) => {
const Client = ws as ExAdminSocketInterface;
try {
Client.disconnecting = true;
socketManager.leaveAdminRoom(Client);
} catch (e) {
console.error('An error occurred on admin "disconnect"');
console.error(e);
}
},
});
}
ioConnection() {
this.app.ws("/room", {
/* Options */
//compression: uWS.SHARED_COMPRESSOR,
idleTimeout: SOCKET_IDLE_TIMER,
maxPayloadLength: 16 * 1024 * 1024,
maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
//idleTimeout: 10,
upgrade: (res, req, context) => {
(async () => {
/* Keep track of abortions */
const upgradeAborted = { aborted: false };
res.onAborted(() => {
/* We can simply signal that we were aborted */
upgradeAborted.aborted = true;
});
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 IPAddress = req.getHeader("x-forwarded-for");
const roomId = query.roomId;
try {
if (typeof roomId !== "string") {
throw new Error("Undefined room ID: ");
}
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;
let companion: CompanionMessage | undefined = undefined;
if (typeof query.companion === "string") {
companion = new CompanionMessage();
companion.setName(query.companion);
}
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 tokenData =
token && typeof token === "string" ? jwtTokenManager.decodeJWTToken(token) : null;
const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = [];
let memberVisitCardUrl: string | null = null;
let memberMessages: unknown;
let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
userUuid: userIdentifier,
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
if (ADMIN_API_URL) {
try {
try {
userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
} catch (err) {
if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn(
'Cannot find user with email "' +
(userIdentifier || "anonymous") +
'". Performing an anonymous login instead.'
);
} else if (err?.response?.status == 403) {
// If we get an HTTP 403, the world is full. We need to broadcast a special error to the client.
// we finish immediately the upgrade then we will close the socket as soon as it starts opening.
return res.upgrade(
{
rejected: true,
message: err?.response?.data.message,
status: err?.response?.status,
},
websocketKey,
websocketProtocol,
websocketExtensions,
context
);
} else {
throw err;
}
}
memberMessages = userData.messages;
memberTags = userData.tags;
memberVisitCardUrl = userData.visitCardUrl;
memberTextures = userData.textures;
if (
room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY &&
(userData.anonymous === true || !room.canAccess(memberTags))
) {
throw new Error("Insufficient privileges to access this room");
}
if (
room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY &&
userData.anonymous === true
) {
throw new Error("Use the login URL to connect");
}
} catch (e) {
console.log(
"access not granted for user " +
(userIdentifier || "anonymous") +
" and room " +
roomId
);
console.error(e);
throw new Error("User cannot access this world");
}
}
// Generate characterLayers objects from characterLayers string[]
const characterLayerObjs: CharacterLayer[] =
SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures);
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,
token,
userUuid: userData.userUuid,
IPAddress,
roomId,
name,
companion,
characterLayers: characterLayerObjs,
messages: memberMessages,
tags: memberTags,
visitCardUrl: memberVisitCardUrl,
textures: memberTextures,
position: {
x: x,
y: y,
direction: "down",
moving: false,
} as PointInterface,
viewport: {
top,
right,
bottom,
left,
},
},
/* Spell these correctly */
websocketKey,
websocketProtocol,
websocketExtensions,
context
);
} catch (e) {
res.upgrade(
{
rejected: true,
reason: e.reason || null,
message: e.message ? e.message : "500 Internal Server Error",
},
websocketKey,
websocketProtocol,
websocketExtensions,
context
);
}
})();
},
/* Handlers */
open: (ws) => {
if (ws.rejected === true) {
//FIX ME to use status code
if (ws.reason === tokenInvalidException) {
socketManager.emitTokenExpiredMessage(ws);
} else if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws);
} else {
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
}
setTimeout(() => ws.close(), 0);
return;
}
// Let's join the room
const client = this.initClient(ws);
socketManager.handleJoinRoom(client);
//get data information and show messages
if (client.messages && Array.isArray(client.messages)) {
client.messages.forEach((c: unknown) => {
const messageToSend = c as { type: string; message: string };
const sendUserMessage = new SendUserMessage();
sendUserMessage.setType(messageToSend.type);
sendUserMessage.setMessage(messageToSend.message);
const serverToClientMessage = new ServerToClientMessage();
serverToClientMessage.setSendusermessage(sendUserMessage);
if (!client.disconnecting) {
client.send(serverToClientMessage.serializeBinary().buffer, true);
}
});
}
},
message: (ws, arrayBuffer, isBinary): void => {
const client = ws as ExSocketInterface;
const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer));
if (message.hasViewportmessage()) {
socketManager.handleViewport(client, (message.getViewportmessage() as ViewportMessage).toObject());
} else if (message.hasUsermovesmessage()) {
socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage);
} else if (message.hasSetplayerdetailsmessage()) {
socketManager.handleSetPlayerDetails(
client,
message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage
);
} else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage);
} else if (message.hasVariablemessage()) {
socketManager.handleVariableEvent(client, message.getVariablemessage() as VariableMessage);
} else if (message.hasWebrtcsignaltoservermessage()) {
socketManager.emitVideo(
client,
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
socketManager.emitScreenSharing(
client,
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
);
} else if (message.hasPlayglobalmessage()) {
socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasReportplayermessage()) {
socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage);
} else if (message.hasQueryjitsijwtmessage()) {
socketManager.handleQueryJitsiJwtMessage(
client,
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
);
} else if (message.hasEmotepromptmessage()) {
socketManager.handleEmotePromptMessage(
client,
message.getEmotepromptmessage() as EmotePromptMessage
);
}
/* Ok is false if backpressure was built up, wait for drain */
//let ok = ws.send(message, isBinary);
},
drain: (ws) => {
console.log("WebSocket backpressure: " + ws.getBufferedAmount());
},
close: (ws, code, message) => {
const Client = ws as ExSocketInterface;
try {
Client.disconnecting = true;
//leave room
socketManager.leaveRoom(Client);
} catch (e) {
console.error('An error occurred on "disconnect"');
console.error(e);
}
},
});
}
//eslint-disable-next-line @typescript-eslint/no-explicit-any
private initClient(ws: any): ExSocketInterface {
const client: ExSocketInterface = ws;
client.userId = this.nextUserId;
this.nextUserId++;
client.userUuid = ws.userUuid;
client.IPAddress = ws.IPAddress;
client.token = ws.token;
client.batchedMessages = new BatchMessage();
client.batchTimeout = null;
client.emitInBatch = (payload: SubMessage): void => {
emitInBatch(client, payload);
};
client.disconnecting = false;
client.messages = ws.messages;
client.name = ws.name;
client.tags = ws.tags;
client.visitCardUrl = ws.visitCardUrl;
client.textures = ws.textures;
client.characterLayers = ws.characterLayers;
client.companion = ws.companion;
client.roomId = ws.roomId;
client.listenedZones = new Set<Zone>();
return client;
}
}