fcf0888864
Now, if the first pusher request fails, a waiting message will be displayed and the application will reconnect when the pusher comes back alive or the network connection is established again.
448 lines
21 KiB
TypeScript
448 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_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
|
|
import { Zone } from "_Model/Zone";
|
|
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
|
|
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;
|
|
let authorizedRoomIds: string[];
|
|
try {
|
|
const data = jwtTokenManager.verifyAdminSocketToken(token as string);
|
|
authorizedRoomIds = data.authorizedRoomIds;
|
|
} catch (e) {
|
|
console.error("Admin access refused for token: " + token);
|
|
res.writeStatus("401 Unauthorized").end("Incorrect token");
|
|
return;
|
|
}
|
|
const roomId = query.roomId;
|
|
if (typeof roomId !== "string" || !authorizedRoomIds.includes(roomId)) {
|
|
console.error("Invalid room id");
|
|
res.writeStatus("403 Bad Request").end("Invalid 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 {
|
|
//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.verifyJWTToken(token) : null;
|
|
|
|
if (DISABLE_ANONYMOUS && !tokenData) {
|
|
throw new Error("Expecting token");
|
|
}
|
|
|
|
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 = {
|
|
email: userIdentifier,
|
|
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;
|
|
}
|
|
}
|