Wrap websockets with HyperExpress

This commit is contained in:
Alexis Faizeau 2022-02-17 11:29:09 +01:00 committed by David Négrier
parent 658781e02e
commit f993aa4f5a
25 changed files with 733 additions and 1018 deletions

View File

@ -41,22 +41,20 @@
"homepage": "https://github.com/thecodingmachine/workadventure#readme", "homepage": "https://github.com/thecodingmachine/workadventure#readme",
"dependencies": { "dependencies": {
"axios": "^0.21.2", "axios": "^0.21.2",
"busboy": "^0.3.1",
"circular-json": "^0.5.9", "circular-json": "^0.5.9",
"debug": "^4.3.1", "debug": "^4.3.1",
"generic-type-guard": "^3.2.0", "generic-type-guard": "^3.2.0",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
"grpc": "^1.24.4", "grpc": "^1.24.4",
"hyper-express": "^5.8.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"openid-client": "^4.7.4", "openid-client": "^4.7.4",
"prom-client": "^12.0.0", "prom-client": "^12.0.0",
"query-string": "^6.13.3", "query-string": "^6.13.3",
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.4.0",
"uuidv4": "^6.0.7" "uuidv4": "^6.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/busboy": "^0.2.3",
"@types/circular-json": "^0.4.0", "@types/circular-json": "^0.4.0",
"@types/debug": "^4.1.5", "@types/debug": "^4.1.5",
"@types/google-protobuf": "^3.7.3", "@types/google-protobuf": "^3.7.3",

View File

@ -4,31 +4,31 @@ import { AuthenticateController } from "./Controller/AuthenticateController"; //
import { MapController } from "./Controller/MapController"; import { MapController } from "./Controller/MapController";
import { PrometheusController } from "./Controller/PrometheusController"; import { PrometheusController } from "./Controller/PrometheusController";
import { DebugController } from "./Controller/DebugController"; import { DebugController } from "./Controller/DebugController";
import { App as uwsApp } from "./Server/sifrr.server";
import { AdminController } from "./Controller/AdminController"; import { AdminController } from "./Controller/AdminController";
import { OpenIdProfileController } from "./Controller/OpenIdProfileController"; import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
import HyperExpress from "hyper-express";
import { cors } from "./Middleware/Cors";
class App { class App {
public app: uwsApp; public app: HyperExpress.compressors.TemplatedApp;
public ioSocketController: IoSocketController;
public authenticateController: AuthenticateController;
public mapController: MapController;
public prometheusController: PrometheusController;
private debugController: DebugController;
private adminController: AdminController;
private openIdProfileController: OpenIdProfileController;
constructor() { constructor() {
this.app = new uwsApp(); const webserver = new HyperExpress.Server();
this.app = webserver.uws_instance;
//create socket controllers // Global middlewares
this.ioSocketController = new IoSocketController(this.app); webserver.use(cors);
this.authenticateController = new AuthenticateController(this.app);
this.mapController = new MapController(this.app); // Socket controllers
this.prometheusController = new PrometheusController(this.app); new IoSocketController(this.app);
this.debugController = new DebugController(this.app);
this.adminController = new AdminController(this.app); // Http controllers
this.openIdProfileController = new OpenIdProfileController(this.app); new AuthenticateController(webserver);
new MapController(webserver);
new PrometheusController(webserver);
new DebugController(webserver);
new AdminController(webserver);
new OpenIdProfileController(webserver);
} }
} }

View File

@ -1,45 +1,22 @@
import { BaseController } from "./BaseController";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import { apiClientRepository } from "../Services/ApiClientRepository"; import { apiClientRepository } from "../Services/ApiClientRepository";
import { import {
AdminRoomMessage, AdminRoomMessage,
WorldFullWarningToRoomMessage, WorldFullWarningToRoomMessage,
RefreshRoomPromptMessage, RefreshRoomPromptMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { adminToken } from "../Middleware/AdminToken";
import { BaseHttpController } from "./BaseHttpController";
export class AdminController extends BaseController { export class AdminController extends BaseHttpController {
constructor(private App: TemplatedApp) { routes() {
super();
this.App = App;
this.receiveGlobalMessagePrompt(); this.receiveGlobalMessagePrompt();
this.receiveRoomEditionPrompt(); this.receiveRoomEditionPrompt();
} }
receiveRoomEditionPrompt() { receiveRoomEditionPrompt() {
this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { this.app.post("/room/refresh", { middlewares: [adminToken] }, async (req, res) => {
res.onAborted(() => { const body = await req.json();
console.warn("/message request was aborted");
});
const token = req.getHeader("admin-token");
const body = await res.json();
if (ADMIN_API_TOKEN === "") {
res.writeStatus("401 Unauthorized").end("No token configured!");
return;
}
if (token !== ADMIN_API_TOKEN) {
console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
return;
}
try { try {
if (typeof body.roomId !== "string") { if (typeof body.roomId !== "string") {
@ -58,41 +35,18 @@ export class AdminController extends BaseController {
}); });
}); });
} catch (err) { } catch (err) {
this.errorToResponse(err, res); this.castErrorToResponse(err, res);
return; return;
} }
res.writeStatus("200"); res.send("ok");
res.end("ok");
}); });
} }
receiveGlobalMessagePrompt() { receiveGlobalMessagePrompt() {
this.App.options("/message", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => { this.app.post("/message", { middlewares: [adminToken] }, async (req, res) => {
res.onAborted(() => { const body = await req.json();
console.warn("/message request was aborted");
});
const token = req.getHeader("admin-token");
const body = await res.json();
if (ADMIN_API_TOKEN === "") {
res.writeStatus("401 Unauthorized").end("No token configured!");
res.end();
return;
}
if (token !== ADMIN_API_TOKEN) {
console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
res.end();
return;
}
try { try {
if (typeof body.text !== "string") { if (typeof body.text !== "string") {
@ -133,13 +87,11 @@ export class AdminController extends BaseController {
}) })
); );
} catch (err) { } catch (err) {
this.errorToResponse(err, res); this.castErrorToResponse(err, res);
return; return;
} }
res.writeStatus("200"); res.send("ok");
this.addCorsHeaders(res);
res.end("ok");
}); });
} }
} }

View File

@ -1,20 +1,18 @@
import { v4 } from "uuid"; import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; import { BaseHttpController } from "./BaseHttpController";
import { BaseController } from "./BaseController";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient"; import { openIDClient } from "../Services/OpenIDClient";
import { DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable"; import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { RegisterData } from "../Messages/JsonMessages/RegisterData"; import { RegisterData } from "../Messages/JsonMessages/RegisterData";
export interface TokenInterface { export interface TokenInterface {
userUuid: string; userUuid: string;
} }
export class AuthenticateController extends BaseController { export class AuthenticateController extends BaseHttpController {
constructor(private App: TemplatedApp) { routes() {
super();
this.openIDLogin(); this.openIDLogin();
this.openIDCallback(); this.openIDCallback();
this.register(); this.register();
@ -24,13 +22,9 @@ export class AuthenticateController extends BaseController {
openIDLogin() { openIDLogin() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises //eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => { this.app.get("/login-screen", async (req, res) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
try { try {
const { nonce, state, playUri, redirect } = parse(req.getQuery()); const { nonce, state, playUri, redirect } = parse(req.path_query);
if (!state || !nonce) { if (!state || !nonce) {
throw new Error("missing state and nonce URL parameters"); throw new Error("missing state and nonce URL parameters");
} }
@ -41,24 +35,22 @@ export class AuthenticateController extends BaseController {
playUri as string | undefined, playUri as string | undefined,
redirect as string | undefined redirect as string | undefined
); );
res.writeStatus("302"); res.status(302);
res.writeHeader("Location", loginUri); res.setHeader("Location", loginUri);
return res.end(); return res;
} catch (e) { } catch (e) {
console.error("openIDLogin => e", e); console.error("openIDLogin => e", e);
return this.errorToResponse(e, res); this.castErrorToResponse(e, res);
return;
} }
}); });
} }
openIDCallback() { openIDCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises //eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => { this.app.get("/login-callback", async (req, res) => {
res.onAborted(() => { const IPAddress = req.header("x-forwarded-for");
console.warn("/message request was aborted"); const { code, nonce, token, playUri } = parse(req.path_query);
});
const IPAddress = req.getHeader("x-forwarded-for");
const { code, nonce, token, playUri } = parse(req.getQuery());
try { try {
//verify connected by token //verify connected by token
if (token != undefined) { if (token != undefined) {
@ -77,23 +69,16 @@ export class AuthenticateController extends BaseController {
//if not nonce and code, user connected in anonymous //if not nonce and code, user connected in anonymous
//get data with identifier and return token //get data with identifier and return token
if (!code && !nonce) { if (!code && !nonce) {
res.writeStatus("200"); return res.json(JSON.stringify({ ...resUserData, authToken: token }));
this.addCorsHeaders(res);
res.writeHeader("Content-Type", "application/json");
return res.end(JSON.stringify({ ...resUserData, authToken: token }));
} }
console.error("Token cannot to be check on OpenId provider"); console.error("Token cannot to be check on OpenId provider");
res.writeStatus("500"); res.status(500);
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL); res.send("User cannot to be connected on openid provider");
res.end("User cannot to be connected on openid provider");
return; return;
} }
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken); const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
res.writeStatus("200"); return res.json({ ...resCheckTokenAuth, ...resUserData, authToken: token });
this.addCorsHeaders(res);
res.writeHeader("Content-Type", "application/json");
return res.end(JSON.stringify({ ...resCheckTokenAuth, ...resUserData, authToken: token }));
} catch (err) { } catch (err) {
console.info("User was not connected", err); console.info("User was not connected", err);
} }
@ -106,9 +91,8 @@ export class AuthenticateController extends BaseController {
} catch (err) { } catch (err) {
//if no access on openid provider, return error //if no access on openid provider, return error
console.error("User cannot to be connected on OpenId provider => ", err); console.error("User cannot to be connected on OpenId provider => ", err);
res.writeStatus("500"); res.status(500);
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL); res.send("User cannot to be connected on openid provider");
res.end("User cannot to be connected on openid provider");
return; return;
} }
const email = userInfo.email || userInfo.sub; const email = userInfo.email || userInfo.sub;
@ -121,23 +105,16 @@ export class AuthenticateController extends BaseController {
//This is very important to create User Local in LocalStorage in WorkAdventure //This is very important to create User Local in LocalStorage in WorkAdventure
const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress); const data = await this.getUserByUserIdentifier(email, playUri as string, IPAddress);
res.writeStatus("200"); return res.json({ ...data, authToken });
this.addCorsHeaders(res);
res.writeHeader("Content-Type", "application/json");
return res.end(JSON.stringify({ ...data, authToken }));
} catch (e) { } catch (e) {
console.error("openIDCallback => ERROR", e); console.error("openIDCallback => ERROR", e);
return this.errorToResponse(e, res); return this.castErrorToResponse(e, res);
} }
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/logout-callback", async (res: HttpResponse, req: HttpRequest) => { this.app.get("/logout-callback", async (req, res) => {
res.onAborted(() => { const { token } = parse(req.path_query);
console.warn("/message request was aborted");
});
const { token } = parse(req.getQuery());
try { try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false); const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
@ -147,29 +124,17 @@ export class AuthenticateController extends BaseController {
await openIDClient.logoutUser(authTokenData.accessToken); await openIDClient.logoutUser(authTokenData.accessToken);
} catch (error) { } catch (error) {
console.error("openIDCallback => logout-callback", error); console.error("openIDCallback => logout-callback", error);
} finally {
res.writeStatus("200");
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
} }
return res;
}); });
} }
//Try to login with an admin token //Try to login with an admin token
private register() { private register() {
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.app.post("/register", (req, res) => {
this.addCorsHeaders(res);
res.end();
});
this.App.post("/register", (res: HttpResponse, req: HttpRequest) => {
(async () => { (async () => {
res.onAborted(() => { const param = await req.json();
console.warn("Login request was aborted");
});
const param = await res.json();
//todo: what to do if the organizationMemberToken is already used? //todo: what to do if the organizationMemberToken is already used?
const organizationMemberToken: string | null = param.organizationMemberToken; const organizationMemberToken: string | null = param.organizationMemberToken;
@ -184,23 +149,18 @@ export class AuthenticateController extends BaseController {
const textures = data.textures; const textures = data.textures;
const authToken = jwtTokenManager.createAuthToken(email || userUuid); const authToken = jwtTokenManager.createAuthToken(email || userUuid);
res.writeStatus("200 OK"); res.json({
this.addCorsHeaders(res); authToken,
res.writeHeader("Content-Type", "application/json"); userUuid,
res.end( email,
JSON.stringify({ roomUrl,
authToken, mapUrlStart,
userUuid, organizationMemberToken,
email, textures,
roomUrl, } as RegisterData);
mapUrlStart,
organizationMemberToken,
textures,
} as RegisterData)
);
} catch (e) { } catch (e) {
console.error("register => ERROR", e); console.error("register => ERROR", e);
this.errorToResponse(e, res); this.castErrorToResponse(e, res);
} }
})(); })();
}); });
@ -208,44 +168,25 @@ export class AuthenticateController extends BaseController {
//permit to login on application. Return token to connect on Websocket IO. //permit to login on application. Return token to connect on Websocket IO.
private anonymLogin() { private anonymLogin() {
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.app.post("/anonymLogin", (req, res) => {
this.addCorsHeaders(res);
res.end();
});
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("Login request was aborted");
});
if (DISABLE_ANONYMOUS) { if (DISABLE_ANONYMOUS) {
res.writeStatus("403 FORBIDDEN"); res.status(403);
res.end(); return res;
} else { } else {
const userUuid = v4(); const userUuid = v4();
const authToken = jwtTokenManager.createAuthToken(userUuid); const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK"); return res.json({
this.addCorsHeaders(res); authToken,
res.writeHeader("Content-Type", "application/json"); userUuid,
res.end( });
JSON.stringify({
authToken,
userUuid,
})
);
} }
}); });
} }
profileCallback() { profileCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile-callback", async (res: HttpResponse, req: HttpRequest) => { this.app.get("/profile-callback", async (req, res) => {
res.onAborted(() => { const { token } = parse(req.path_query);
console.warn("/message request was aborted");
});
const { token } = parse(req.getQuery());
try { try {
//verify connected by token //verify connected by token
if (token != undefined) { if (token != undefined) {
@ -257,18 +198,17 @@ export class AuthenticateController extends BaseController {
await openIDClient.checkTokenAuth(authTokenData.accessToken); await openIDClient.checkTokenAuth(authTokenData.accessToken);
//get login profile //get login profile
res.writeStatus("302"); res.status(302);
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken)); res.setHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
this.addCorsHeaders(res); return;
// eslint-disable-next-line no-unsafe-finally
return res.end();
} catch (error) { } catch (error) {
return this.errorToResponse(error, res); this.castErrorToResponse(error, res);
return;
} }
} }
} catch (error) { } catch (error) {
console.error("profileCallback => ERROR", error); console.error("profileCallback => ERROR", error);
this.errorToResponse(error, res); this.castErrorToResponse(error, res);
} }
}); });
} }

View File

@ -1,45 +0,0 @@
import { HttpResponse } from "uWebSockets.js";
import { FRONT_URL } from "../Enum/EnvironmentVariable";
export class BaseController {
protected addCorsHeaders(res: HttpResponse): void {
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
res.writeHeader("access-control-allow-origin", FRONT_URL);
}
/**
* Turns any exception into a HTTP response (and logs the error)
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected errorToResponse(e: any, res: HttpResponse): void {
if (e && e.message) {
let url = e?.config?.url;
if (url !== undefined) {
url = " for URL: " + url;
} else {
url = "";
}
console.error("ERROR: " + e.message + url);
} else if (typeof e === "string") {
console.error(e);
}
if (e.stack) {
console.error(e.stack);
}
if (e.response) {
res.writeStatus(e.response.status + " " + e.response.statusText);
this.addCorsHeaders(res);
res.end(
"An error occurred: " +
e.response.status +
" " +
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
);
} else {
res.writeStatus("500 Internal Server Error");
this.addCorsHeaders(res);
res.end("An error occurred");
}
}
}

View File

@ -0,0 +1,45 @@
import { Server } from "hyper-express";
import Response from "hyper-express/types/components/http/Response";
import axios from "axios";
export class BaseHttpController {
constructor(protected app: Server) {
this.routes();
}
protected routes() {
/* Define routes on children */
}
protected castErrorToResponse(e: unknown, res: Response): void {
if (e instanceof Error) {
let url: string | undefined;
if (axios.isAxiosError(e)) {
url = e.config.url;
if (url !== undefined) {
url = " for URL: " + url;
} else {
url = "";
}
}
console.error("ERROR: " + e.message + url);
console.error(e.stack);
} else if (typeof e === "string") {
console.error(e);
}
if (axios.isAxiosError(e) && e.response) {
res.status(e.response.status);
res.send(
"An error occurred: " +
e.response.status +
" " +
(e.response.data && e.response.data.message ? e.response.data.message : e.response.statusText)
);
} else {
res.status(500);
res.send("An error occurred");
}
}
}

View File

@ -1,51 +1,42 @@
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
import { IoSocketController } from "_Controller/IoSocketController";
import { stringify } from "circular-json"; import { stringify } from "circular-json";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { parse } from "query-string"; import { parse } from "query-string";
import { App } from "../Server/sifrr.server";
import { socketManager } from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
import { BaseHttpController } from "./BaseHttpController";
export class DebugController { export class DebugController extends BaseHttpController {
constructor(private App: App) { routes() {
this.getDump(); this.app.get("/dump", (req, res) => {
} const query = parse(req.path_query);
getDump() {
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery());
if (ADMIN_API_TOKEN === "") { if (ADMIN_API_TOKEN === "") {
return res.writeStatus("401 Unauthorized").end("No token configured!"); return res.status(401).send("No token configured!");
} }
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); return res.status(401).send("Invalid token sent!");
} }
const worlds = Object.fromEntries(socketManager.getWorlds().entries()); const worlds = Object.fromEntries(socketManager.getWorlds().entries());
return res return res.json(
.writeStatus("200 OK") stringify(worlds, (key: unknown, value: unknown) => {
.writeHeader("Content-Type", "application/json") if (value instanceof Map) {
.end( const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
stringify(worlds, (key: unknown, value: unknown) => { for (const [mapKey, mapValue] of value.entries()) {
if (value instanceof Map) { obj[mapKey] = mapValue;
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
} }
}) return obj;
); } else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
})
);
}); });
} }
} }

View File

@ -1,5 +1,5 @@
import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
import { GameRoomPolicyTypes, PusherRoom } from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { PointInterface } from "../Model/Websocket/PointInterface"; import { PointInterface } from "../Model/Websocket/PointInterface";
import { import {
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
@ -23,7 +23,6 @@ import {
VariableMessage, VariableMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import { UserMovesMessage } from "../Messages/generated/messages_pb"; import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string"; import { parse } from "query-string";
import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager"; import { AdminSocketTokenData, jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
@ -36,11 +35,12 @@ import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages"; import { isAdminMessageInterface } from "../Model/Websocket/Admin/AdminMessages";
import Axios from "axios"; import Axios from "axios";
import { InvalidTokenError } from "../Controller/InvalidTokenError"; import { InvalidTokenError } from "../Controller/InvalidTokenError";
import HyperExpress from "hyper-express";
export class IoSocketController { export class IoSocketController {
private nextUserId: number = 1; private nextUserId: number = 1;
constructor(private readonly app: TemplatedApp) { constructor(private readonly app: HyperExpress.compressors.TemplatedApp) {
this.ioConnection(); this.ioConnection();
if (ADMIN_SOCKETS_TOKEN) { if (ADMIN_SOCKETS_TOKEN) {
this.adminRoomSocket(); this.adminRoomSocket();

View File

@ -1,41 +1,21 @@
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { parse } from "query-string";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { ADMIN_API_URL, DISABLE_ANONYMOUS, FRONT_URL } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { GameRoomPolicyTypes } from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; import { isMapDetailsData, MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { socketManager } from "../Services/SocketManager";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { v4 } from "uuid";
import { InvalidTokenError } from "./InvalidTokenError"; import { InvalidTokenError } from "./InvalidTokenError";
import { parse } from "query-string";
import { BaseHttpController } from "./BaseHttpController";
export class MapController extends BaseController { export class MapController extends BaseHttpController {
constructor(private App: TemplatedApp) {
super();
this.App = App;
this.getMapUrl();
}
// Returns a map mapping map name to file name of the map // Returns a map mapping map name to file name of the map
getMapUrl() { routes() {
this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { this.app.get("/map", (req, res) => {
this.addCorsHeaders(res); const query = parse(req.path_query);
res.end();
});
this.App.get("/map", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/map request was aborted");
});
const query = parse(req.getQuery());
if (typeof query.playUri !== "string") { if (typeof query.playUri !== "string") {
console.error("Expected playUri parameter in /map endpoint"); console.error("Expected playUri parameter in /map endpoint");
res.writeStatus("400 Bad request"); res.status(400);
this.addCorsHeaders(res); res.send("Expected playUri parameter");
res.end("Expected playUri parameter");
return; return;
} }
@ -45,30 +25,23 @@ export class MapController extends BaseController {
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname); const match = /\/_\/[^/]+\/(.+)/.exec(roomUrl.pathname);
if (!match) { if (!match) {
res.writeStatus("404 Not Found"); res.status(404);
this.addCorsHeaders(res); res.json({});
res.writeHeader("Content-Type", "application/json");
res.end(JSON.stringify({}));
return; return;
} }
const mapUrl = roomUrl.protocol + "//" + match[1]; const mapUrl = roomUrl.protocol + "//" + match[1];
res.writeStatus("200 OK"); res.json({
this.addCorsHeaders(res); mapUrl,
res.writeHeader("Content-Type", "application/json"); policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY,
res.end( roomSlug: null, // Deprecated
JSON.stringify({ group: null,
mapUrl, tags: [],
policy_type: GameRoomPolicyTypes.ANONYMOUS_POLICY, textures: [],
roomSlug: null, // Deprecated contactPage: null,
group: null, authenticationMandatory: DISABLE_ANONYMOUS,
tags: [], } as MapDetailsData);
textures: [],
contactPage: null,
authenticationMandatory: DISABLE_ANONYMOUS,
} as MapDetailsData)
);
return; return;
} }
@ -90,12 +63,12 @@ export class MapController extends BaseController {
} catch (e) { } catch (e) {
if (e instanceof InvalidTokenError) { if (e instanceof InvalidTokenError) {
// The token was not good, redirect user on login page // The token was not good, redirect user on login page
res.writeStatus("401 Unauthorized"); res.status(401);
res.writeHeader("Access-Control-Allow-Origin", FRONT_URL); res.send("Token decrypted error");
res.end("Token decrypted error");
return; return;
} else { } else {
return this.errorToResponse(e, res); this.castErrorToResponse(e, res);
return;
} }
} }
} }
@ -106,12 +79,9 @@ export class MapController extends BaseController {
mapDetails.authenticationMandatory = true; mapDetails.authenticationMandatory = true;
} }
res.writeStatus("200 OK"); res.json(mapDetails);
this.addCorsHeaders(res);
res.writeHeader("Content-Type", "application/json");
res.end(JSON.stringify(mapDetails));
} catch (e) { } catch (e) {
this.errorToResponse(e, res); this.castErrorToResponse(e, res);
} }
})(); })();
}); });

View File

@ -1,26 +1,13 @@
import { BaseController } from "./BaseController";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient"; import { openIDClient } from "../Services/OpenIDClient";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { adminApi } from "../Services/AdminApi";
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable"; import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
import { IntrospectionResponse } from "openid-client"; import { BaseHttpController } from "./BaseHttpController";
export class OpenIdProfileController extends BaseController { export class OpenIdProfileController extends BaseHttpController {
constructor(private App: TemplatedApp) { routes() {
super();
this.profileOpenId();
}
profileOpenId() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises //eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile", async (res: HttpResponse, req: HttpRequest) => { this.app.get("/profile", async (req, res) => {
res.onAborted(() => { const { accessToken } = parse(req.path_query);
console.warn("/message request was aborted");
});
const { accessToken } = parse(req.getQuery());
if (!accessToken) { if (!accessToken) {
throw Error("Access token expected cannot to be check on Hydra"); throw Error("Access token expected cannot to be check on Hydra");
} }
@ -29,7 +16,7 @@ export class OpenIdProfileController extends BaseController {
if (!resCheckTokenAuth.email) { if (!resCheckTokenAuth.email) {
throw new Error("Email was not found"); throw new Error("Email was not found");
} }
res.end( res.send(
this.buildHtml( this.buildHtml(
OPID_CLIENT_ISSUER, OPID_CLIENT_ISSUER,
resCheckTokenAuth.email as string, resCheckTokenAuth.email as string,
@ -38,7 +25,7 @@ export class OpenIdProfileController extends BaseController {
); );
} catch (error) { } catch (error) {
console.error("profileCallback => ERROR", error); console.error("profileCallback => ERROR", error);
this.errorToResponse(error, res); this.castErrorToResponse(error, res);
} }
}); });
} }

View File

@ -1,18 +1,23 @@
import { App } from "../Server/sifrr.server";
import { HttpRequest, HttpResponse } from "uWebSockets.js";
import { register, collectDefaultMetrics } from "prom-client"; import { register, collectDefaultMetrics } from "prom-client";
import { Server } from "hyper-express";
import { BaseHttpController } from "./BaseHttpController";
import Request from "hyper-express/types/components/http/Request";
import Response from "hyper-express/types/components/http/Response";
export class PrometheusController { export class PrometheusController extends BaseHttpController {
constructor(private App: App) { constructor(app: Server) {
super(app);
collectDefaultMetrics({ collectDefaultMetrics({
gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets.
}); });
this.App.get("/metrics", this.metrics.bind(this));
} }
private metrics(res: HttpResponse, req: HttpRequest): void { routes() {
res.writeHeader("Content-Type", register.contentType); this.app.get("/metrics", this.metrics.bind(this));
}
private metrics(req: Request, res: Response): void {
res.setHeader("Content-Type", register.contentType);
res.end(register.metrics()); res.end(register.metrics());
} }
} }

View File

@ -0,0 +1,22 @@
import Request from "hyper-express/types/components/http/Request";
import Response from "hyper-express/types/components/http/Response";
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
export function adminToken(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
const token = req.header("admin-token");
if (ADMIN_API_TOKEN === "") {
res.status(401).end("No token configured!");
return;
}
if (token !== ADMIN_API_TOKEN) {
console.error("Admin access refused for token: " + token);
res.status(401).end("Incorrect token");
return;
}
if (next) {
next();
}
}

View File

@ -0,0 +1,14 @@
import Request from "hyper-express/types/components/http/Request";
import Response from "hyper-express/types/components/http/Response";
import { MiddlewareNext, MiddlewarePromise } from "hyper-express/types/components/router/Router";
import { FRONT_URL } from "../Enum/EnvironmentVariable";
export function cors(req: Request, res: Response, next?: MiddlewareNext): MiddlewarePromise {
res.setHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
res.setHeader("access-control-allow-origin", FRONT_URL);
if (next) {
next();
}
}

View File

@ -9,13 +9,13 @@ import {
ServerToClientMessage, ServerToClientMessage,
SubMessage, SubMessage,
} from "../../Messages/generated/messages_pb"; } from "../../Messages/generated/messages_pb";
import { WebSocket } from "uWebSockets.js"; import { compressors } from "hyper-express";
import { ClientDuplexStream } from "grpc"; import { ClientDuplexStream } from "grpc";
import { Zone } from "_Model/Zone"; import { Zone } from "_Model/Zone";
export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>; export type AdminConnection = ClientDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
export interface ExAdminSocketInterface extends WebSocket { export interface ExAdminSocketInterface extends compressors.WebSocket {
adminConnection: AdminConnection; adminConnection: AdminConnection;
disconnecting: boolean; disconnecting: boolean;
} }

View File

@ -12,6 +12,7 @@ import { WebSocket } from "uWebSockets.js";
import { ClientDuplexStream } from "grpc"; import { ClientDuplexStream } from "grpc";
import { Zone } from "_Model/Zone"; import { Zone } from "_Model/Zone";
import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture"; import { CharacterTexture } from "../../Messages/JsonMessages/CharacterTexture";
import { compressors } from "hyper-express";
export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>; export type BackConnection = ClientDuplexStream<PusherToBackMessage, ServerToClientMessage>;
@ -20,7 +21,7 @@ export interface CharacterLayer {
url: string | undefined; url: string | undefined;
} }
export interface ExSocketInterface extends WebSocket, Identificable { export interface ExSocketInterface extends compressors.WebSocket, Identificable {
token: string; token: string;
roomId: string; roomId: string;
//userId: number; // A temporary (autoincremented) identifier for this user //userId: number; // A temporary (autoincremented) identifier for this user

View File

@ -1,13 +0,0 @@
import { App as _App, AppOptions } from "uWebSockets.js";
import BaseApp from "./baseapp";
import { extend } from "./utils";
import { UwsApp } from "./types";
class App extends (<UwsApp>_App) {
constructor(options: AppOptions = {}) {
super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp());
}
}
export default App;

View File

@ -1,109 +0,0 @@
import { Readable } from "stream";
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
import formData from "./formdata";
import { stob } from "./utils";
import { Handler } from "./types";
import { join } from "path";
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
const noOp = () => true;
const handleBody = (res: HttpResponse, req: HttpRequest) => {
const contType = req.getHeader("content-type");
res.bodyStream = function () {
const stream = new Readable();
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
this.onData((ab: ArrayBuffer, isLast: boolean) => {
// uint and then slicing is bit faster than slice and then uint
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
if (isLast) {
stream.push(null);
}
});
return stream;
};
res.body = () => stob(res.bodyStream());
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
};
class BaseApp {
_sockets = new Map();
ws!: TemplatedApp["ws"];
get!: TemplatedApp["get"];
_post!: TemplatedApp["post"];
_put!: TemplatedApp["put"];
_patch!: TemplatedApp["patch"];
_listen!: TemplatedApp["listen"];
post(pattern: string, handler: Handler) {
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._post(pattern, (res, req) => {
handleBody(res, req);
handler(res, req);
});
return this;
}
put(pattern: string, handler: Handler) {
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._put(pattern, (res, req) => {
handleBody(res, req);
handler(res, req);
});
return this;
}
patch(pattern: string, handler: Handler) {
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
this._patch(pattern, (res, req) => {
handleBody(res, req);
handler(res, req);
});
return this;
}
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
if (typeof p === "number" && typeof h === "string") {
this._listen(h, p, (socket) => {
this._sockets.set(p, socket);
if (cb === undefined) {
throw new Error("cb undefined");
}
cb(socket);
});
} else if (typeof h === "number" && typeof p === "function") {
this._listen(h, (socket) => {
this._sockets.set(h, socket);
p(socket);
});
} else {
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
}
return this;
}
close(port: null | number = null) {
if (port) {
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
this._sockets.delete(port);
} else {
this._sockets.forEach((app) => {
us_listen_socket_close(app);
});
this._sockets.clear();
}
return this;
}
}
export default BaseApp;

View File

@ -1,99 +0,0 @@
import { createWriteStream } from "fs";
import { join, dirname } from "path";
import Busboy from "busboy";
import mkdirp from "mkdirp";
function formData(
contType: string,
options: busboy.BusboyConfig & {
abortOnLimit?: boolean;
tmpDir?: string;
onFile?: (
fieldname: string,
file: NodeJS.ReadableStream,
filename: string,
encoding: string,
mimetype: string
) => string;
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
filename?: (oldName: string) => string;
} = {}
) {
console.log("Enter form data");
options.headers = {
"content-type": contType,
};
return new Promise((resolve, reject) => {
const busb = new Busboy(options);
const ret = {};
this.bodyStream().pipe(busb);
busb.on("limit", () => {
if (options.abortOnLimit) {
reject(Error("limit"));
}
});
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
filename,
encoding,
mimetype,
filePath: undefined,
};
if (typeof options.tmpDir === "string") {
if (typeof options.filename === "function") filename = options.filename(filename);
const fileToSave = join(options.tmpDir, filename);
mkdirp(dirname(fileToSave));
file.pipe(createWriteStream(fileToSave));
value.filePath = fileToSave;
}
if (typeof options.onFile === "function") {
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
}
setRetValue(ret, fieldname, value);
});
busb.on("field", function (fieldname, value) {
if (typeof options.onField === "function") options.onField(fieldname, value);
setRetValue(ret, fieldname, value);
});
busb.on("finish", function () {
resolve(ret);
});
busb.on("error", reject);
});
}
function setRetValue(
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
fieldname: string,
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
) {
if (fieldname.endsWith("[]")) {
fieldname = fieldname.slice(0, fieldname.length - 2);
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else {
ret[fieldname] = [value];
}
} else {
if (Array.isArray(ret[fieldname])) {
ret[fieldname].push(value);
} else if (ret[fieldname]) {
ret[fieldname] = [ret[fieldname], value];
} else {
ret[fieldname] = value;
}
}
}
export default formData;

View File

@ -1,13 +0,0 @@
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
import BaseApp from "./baseapp";
import { extend } from "./utils";
import { UwsApp } from "./types";
class SSLApp extends (<UwsApp>_SSLApp) {
constructor(options: AppOptions) {
super(options); // eslint-disable-line constructor-super
extend(this, new BaseApp());
}
}
export default SSLApp;

View File

@ -1,11 +0,0 @@
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
export type UwsApp = {
(options: AppOptions): TemplatedApp;
new (options: AppOptions): TemplatedApp;
prototype: TemplatedApp;
};
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
export {};

View File

@ -1,36 +0,0 @@
import { ReadStream } from "fs";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extend(who: any, from: any, overwrite = true) {
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
ownProps.forEach((prop) => {
if (prop === "constructor" || from[prop] === undefined) return;
if (who[prop] && overwrite) {
who[`_${prop}`] = who[prop];
}
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
else who[prop] = from[prop];
});
}
function stob(stream: ReadStream): Promise<Buffer> {
return new Promise((resolve) => {
const buffers: Buffer[] = [];
stream.on("data", buffers.push.bind(buffers));
stream.on("end", () => {
switch (buffers.length) {
case 0:
resolve(Buffer.allocUnsafe(0));
break;
case 1:
resolve(buffers[0]);
break;
default:
resolve(Buffer.concat(buffers));
}
});
});
}
export { extend, stob };

View File

@ -1,19 +0,0 @@
import { parse } from "query-string";
import { HttpRequest } from "uWebSockets.js";
import App from "./server/app";
import SSLApp from "./server/sslapp";
import * as types from "./server/types";
const getQuery = (req: HttpRequest) => {
return parse(req.getQuery());
};
export { App, SSLApp, getQuery };
export * from "./server/types";
export default {
App,
SSLApp,
getQuery,
...types,
};

View File

@ -1,6 +1,5 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import Axios from "axios"; import Axios from "axios";
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture"; import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData"; import { MapDetailsData } from "../Messages/JsonMessages/MapDetailsData";
import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; import { RoomRedirect } from "../Messages/JsonMessages/RoomRedirect";

View File

@ -53,6 +53,7 @@ import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"
import { WebSocket } from "uWebSockets.js"; import { WebSocket } from "uWebSockets.js";
import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect"; import { isRoomRedirect } from "../Messages/JsonMessages/RoomRedirect";
import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture"; import { CharacterTexture } from "../Messages/JsonMessages/CharacterTexture";
import { compressors } from "hyper-express";
const debug = Debug("socket"); const debug = Debug("socket");
@ -619,7 +620,7 @@ export class SocketManager implements ZoneEventListener {
emitInBatch(listener, subMessage); emitInBatch(listener, subMessage);
} }
public emitWorldFullMessage(client: WebSocket) { public emitWorldFullMessage(client: compressors.WebSocket) {
const errorMessage = new WorldFullMessage(); const errorMessage = new WorldFullMessage();
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
@ -630,7 +631,7 @@ export class SocketManager implements ZoneEventListener {
} }
} }
public emitTokenExpiredMessage(client: WebSocket) { public emitTokenExpiredMessage(client: compressors.WebSocket) {
const errorMessage = new TokenExpiredMessage(); const errorMessage = new TokenExpiredMessage();
const serverToClientMessage = new ServerToClientMessage(); const serverToClientMessage = new ServerToClientMessage();
@ -641,7 +642,7 @@ export class SocketManager implements ZoneEventListener {
} }
} }
public emitConnexionErrorMessage(client: WebSocket, message: string) { public emitConnexionErrorMessage(client: compressors.WebSocket, message: string) {
const errorMessage = new WorldConnexionMessage(); const errorMessage = new WorldConnexionMessage();
errorMessage.setMessage(message); errorMessage.setMessage(message);

File diff suppressed because it is too large Load Diff