Verified Commit d3004083 authored by Maxime FRIESS's avatar Maxime FRIESS 💙
Browse files

[*] Switched to TypeScript

parent 2bea3af0
config/hardconfig.json
config/softconfig.json
node_modules
dist
cache.json
*.log
.env
......@@ -17,6 +17,6 @@
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import main from './src/main.js';
import main from './src/main';
main();
{
"name": "seb-gateway",
"version": "dev",
"main": "index.js",
"main": "index.ts",
"repository": "https://git.unistra.fr/amicale-core/seb-gateway",
"author": "M4x1m3 <M4x1me@protonmail.com>",
"license": "AGPLv3",
"private": false,
"type": "module",
"dependencies": {
"@types/node": "^16.11.6",
"@types/ws": "^8.2.0",
"dotenv": "^10.0.0",
"typescript": "^4.4.4",
"ws": "^8.2.3"
},
"scripts": {
"start": "node index.js",
"debug": "nodemon index.js"
"build": "tsc",
"start": "node ./dist/index.js",
"debug": "ts-node index.ts",
"prod": "yarn build && yarn start"
},
"devDependencies": {
"nodemon": "^2.0.13"
"nodemon": "^2.0.13",
"ts": "^0.2.2",
"ts-node": "^10.4.0"
}
}
import { createServer } from 'http';
import HardConfig from './config/HardConfig.js';
import Gateway from './Gateway.js';
import Loggers from './utils/Logger.js';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import HardConfig from './config/HardConfig';
import Gateway from './Gateway';
import Loggers, { Logger } from './utils/Logger';
class _API {
private server: Server;
private logger: Logger;
constructor() {
this.__server = createServer(this.request.bind(this));
this.__logger = Loggers.getLogger("API");
this.server = createServer(this.request.bind(this));
this.logger = Loggers.getLogger("API");
}
start() {
this.__logger.info(`Starting api server on ${HardConfig.getHttpIp()}:${HardConfig.getHttpPort()}...`);
this.__server.listen(HardConfig.getHttpPort(), HardConfig.getHttpIp());
start(): void {
this.logger.info(`Starting api server on ${HardConfig.getHttpIp()}:${HardConfig.getHttpPort()}...`);
this.server.listen(HardConfig.getHttpPort(), HardConfig.getHttpIp());
}
__log(req, code) {
const ip = HardConfig.getHttpProxy() ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
this.__logger.info(`Request from ${ip}: ${req.method} ${req.url} (${code})`);
private log(req: IncomingMessage, code: number) {
const ip = HardConfig.getHttpProxy() ? (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0].trim() : req.socket?.remoteAddress;
this.logger.info(`Request from ${ip}: ${req.method} ${req.url} (${code})`);
}
handle_event(name, data) {
handle_event(name: string, data: any) {
return Gateway.broadcastEvent(name, data);
}
request(req, res) {
if (this.__auth(req)) {
let result = req.url.match("^/event/([a-z\.]+)$");
if (req.method == "POST" && result) {
request(req: IncomingMessage, res: ServerResponse) {
if (this.auth(req)) {
let result = req.url?.match("^/event/([a-z\.]+)$");
if (req.method == "POST" && result !== undefined && result !== null) {
let body = "";
req.setEncoding('utf8');
req.on('data', (chunk) => {
......@@ -36,26 +40,26 @@ class _API {
req.on('end', () => {
try {
let data = JSON.parse(body);
if (this.handle_event(result[1], data)) {
if (this.handle_event((result as Array<any>)[1], data)) {
res.writeHead(200);
res.end();
this.__log(req, 200);
this.log(req, 200);
} else {
res.writeHead(400);
res.end();
this.__log(req, 400);
this.log(req, 400);
}
} catch (e) {
res.writeHead(400);
res.end();
this.__log(req, 400);
this.log(req, 400);
}
})
} else {
res.writeHead(404);
res.end();
this.__log(req, 404);
this.log(req, 404);
}
} else {
......@@ -63,11 +67,11 @@ class _API {
"WWW-Authenticate": "Bearer"
});
res.end();
this.__log(req, 401);
this.log(req, 401);
}
}
__auth(req) {
private auth(req: IncomingMessage) {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
const token = req.headers.authorization.split(' ')[1];
return token === HardConfig.getHttpToken();
......
......@@ -17,50 +17,83 @@
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import Gateway from "./Gateway.js";
import Loggers from "./utils/Logger.js";
import { RawData, WebSocket } from 'ws';
import Gateway from "./Gateway";
import Loggers, { Logger } from "./utils/Logger";
let lastId = 0;
/**
* Represents a client connected to the Gateway
*/
class Client {
constructor(ws) {
this.__id = lastId++;
this.__logger = Loggers.getLogger("Client", this.__id);
this.__logger.info("Accepted connection.");
this.__ws = ws;
this.__ws.on("close", this.close.bind(this));
this.__ws.on("pong", this.heartbeat.bind(this));
this.__ws.on("message", this.message.bind(this));
this.__alive = true;
private id: number;
private logger: Logger;
private ws: WebSocket;
private alive: boolean;
constructor(ws: WebSocket) {
this.id = lastId++;
this.logger = Loggers.getLogger("Client", this.id);
this.logger.info("Accepted connection.");
this.ws = ws;
this.ws.on("close", this.close.bind(this));
this.ws.on("pong", this.heartbeat.bind(this));
this.ws.on("message", this.message.bind(this));
this.alive = true;
}
getId() {
return this.__id;
/**
* Get the ID of the client
*
* @returns The ID of the client
*/
getId(): number {
return this.id;
}
send(data) {
this.__ws.send(data);
/**
* Send data to the client
*
* @param {*} data
*/
send(data: any): void {
this.ws.send(data);
}
message(data) {
/**
* Handler called when a message is received
*/
private message(data: RawData, isBinary: boolean): void {
}
heartbeat() {
this.__alive = true;
/**
* Function called when the client sends a ping
*/
private heartbeat(): void {
this.alive = true;
}
ping() {
if (!this.__alive)
return this.__ws.terminate();
/**
* Called every 10s. Sends a ping to the client,
* disconnects it if it didn't respond to the last ping.
*
*/
ping(): void {
if (!this.alive)
return this.ws.terminate();
this.__alive = false;
this.__ws.ping();
this.alive = false;
this.ws.ping();
}
close() {
this.__logger.info("Closed connection.");
/**
* Closes the conection.
*/
private close(): void {
this.logger.info("Closed connection.");
Gateway.removeClient(this);
}
}
......
/**
* Copyright © 2021 Maxime Friess <M4x1me@pm.me>
*
* This file is part of Seb-Gateway.
*
* Seb-Gateway is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Seb-Gateway is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import Client from './Client.js';
import HardConfig from './config/HardConfig.js';
import Loggers from './utils/Logger.js';
class _Gateway {
constructor() {
this.__server = createServer();
this.__ws = new WebSocketServer({ server: this.__server });
this.__ws.on('connection', this.__connect.bind(this));
this.__logger = Loggers.getLogger("Gateway");
this.__clients = {};
}
__connect(ws, req) {
const ip = HardConfig.getWsProxy() ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
this.__logger.info(`Accepted connection from ${ip}.`);
const client = new Client(ws);
this.__clients[client.getId()] = client;
}
start() {
this.__logger.info(`Starting gateway on ${HardConfig.getWsIp()}:${HardConfig.getWsPort()}...`);
this.__server.listen(HardConfig.getWsPort(), HardConfig.getWsIp());
this.__interval = setInterval(this.ping.bind(this), 10000);
}
ping() {
for (let k of Object.keys(this.__clients)) {
this.__clients[k].ping();
}
}
broadcast(string) {
for (let k of Object.keys(this.__clients)) {
this.__clients[k].send(string);
}
}
removeClient(client) {
delete this.__clients[client.getId()];
}
broadcastEvent(name, data) {
const event_string = JSON.stringify({p: name, d: data}) + "\n";
this.broadcast(event_string);
this.__logger.info(`Broadcasted event ${name}: ${JSON.stringify(data)}`);
return true;
}
}
const Gateway = new _Gateway();
export default Gateway;
/**
* Copyright © 2021 Maxime Friess <M4x1me@pm.me>
*
* This file is part of Seb-Gateway.
*
* Seb-Gateway is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Seb-Gateway is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import { createServer, IncomingMessage, Server } from 'http';
import { WebSocket, WebSocketServer } from 'ws';
import Client from './Client';
import HardConfig from './config/HardConfig';
import Loggers, { Logger } from './utils/Logger';
/**
* Gateway server. Accepts WebSockets connection and dispatches event to them.
*/
class _Gateway {
private server: Server;
private ws: WebSocketServer;
private logger: Logger;
private clients: { [index: string]: Client };
constructor() {
this.server = createServer();
this.ws = new WebSocketServer({ server: this.server });
this.ws.on('connection', this.connect.bind(this));
this.logger = Loggers.getLogger("Gateway");
this.clients = {};
}
/**
* Connection handler. Creates a Client object and stores it.
*
* @param {WebSocket} ws
* @param {http.IncomingMessage} req
*/
private connect(ws: WebSocket, req: IncomingMessage): void {
const ip = HardConfig.getWsProxy() ? (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0].trim() : req.socket.remoteAddress;
this.logger.info(`Accepted connection from ${ip}.`);
const client = new Client(ws);
this.clients[client.getId()] = client;
}
/**
* Starts the server
*/
start(): void {
this.logger.info(`Starting gateway on ${HardConfig.getWsIp()}:${HardConfig.getWsPort()}...`);
this.server.listen(HardConfig.getWsPort(), HardConfig.getWsIp());
setInterval(this.ping.bind(this), 10000);
}
/**
* Pings all connecred clients
*/
ping(): void {
for (let k of Object.keys(this.clients)) {
this.clients[k].ping();
}
}
/**
* Broadcasts a message to all connected clients.
* @param {string} message
*/
broadcast(message: string): void {
for (let k of Object.keys(this.clients)) {
this.clients[k].send(message);
}
}
/**
* Removes a client from the list of connected clients.
*
* @param {Client} client
*/
removeClient(client: Client): void {
delete this.clients[client.getId()];
}
/**
* Broadcasts an event to the connected clients.
*
* @param {String} name
* @param {*} data
* @returns
*/
broadcastEvent(name: string, data: any): boolean {
const event_string = JSON.stringify({ p: name, d: data }) + "\n";
this.broadcast(event_string);
this.logger.info(`Broadcasted event ${name}: ${JSON.stringify(data)}`);
return true;
}
}
const Gateway = new _Gateway();
export default Gateway;
......@@ -17,9 +17,7 @@
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import dotenv from 'dotenv';
const HARDCONFIG_FILE = 'config/hardconfig.json';
import { config } from 'dotenv';
/**
* Class that parses the hardconfig.json file.
......@@ -29,35 +27,35 @@ const HARDCONFIG_FILE = 'config/hardconfig.json';
*/
class _HardConfig {
constructor() {
dotenv.config();
config();
}
getWsIp() {
return process.env.WS_IP;
getWsIp(): string {
return process.env.WS_IP ?? "127.0.0.1";
}
getWsPort() {
return process.env.WS_PORT;
getWsPort(): number {
return process.env.WS_PORT === undefined ? 8080 : parseInt(process.env.WS_PORT);
}
getWsProxy() {
return process.env.WS_PROXY == "true";
getWsProxy(): boolean {
return process.env.WS_PROXY === undefined ? false : process.env.WS_PROXY == "true";
}
getHttpIp() {
return process.env.HTTP_IP;
getHttpIp(): string {
return process.env.HTTP_IP ?? "127.0.0.1";
}
getHttpPort() {
return process.env.HTTP_PORT;
getHttpPort(): number {
return process.env.HTTP_PORT === undefined ? 8081 : parseInt(process.env.HTTP_PORT);
}
getHttpToken() {
return process.env.HTTP_TOKEN;
getHttpToken(): string {
return process.env.HTTP_TOKEN ?? "";
}
getHttpProxy() {
return process.env.HTTP_PROXY == "true";
getHttpProxy(): boolean {
return process.env.HTTP_PROXY === undefined ? false : process.env.HTTP_PROXY == "true";
}
}
......
......@@ -17,11 +17,11 @@
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import API from "./API.js";
import Gateway from "./Gateway.js";
import Loggers from "./utils/Logger.js";
import API from "./API";
import Gateway from "./Gateway";
import Loggers from "./utils/Logger";
const main = () => {
const main: () => void = () => {
Loggers.getLogger("Main").info("Starting Seb-Gateway");
Gateway.start();
API.start();
......
......@@ -17,27 +17,29 @@
* along with Seb-Gateway. If not, see <https://www.gnu.org/licenses/>.
*/
import process from "process";
import { exit } from "process";
/**
* Logger, used to log things.
*/
class Logger {
private name: string;
/**
* Construct a new logger
*
* @param {string} name Name of the logger
*/
constructor(name) {
this.__name = name;
constructor(name: string) {
this.name = name;
}
/**
* Get the name of the logger
* @returns Name of the logger
*/
getName() {
return this.__name;
getName(): string {
return this.name;
}
/**
......@@ -47,49 +49,51 @@ class Logger {
* @param {string} prefix Prefix
* @param {function} logfnc Function to call to write
*/
__write(string, prefix, logfnc) {
private write(string: string, prefix: string, logfnc: (...data: any[]) => void): void {
const date = new Date().toISOString();
const pref = "[" + date + "][" + this.__name + "][" + prefix + "] ";
const pref = "[" + date + "][" + this.name + "][" + prefix + "] ";
for (let line of string.toString().split('\n')) {
logfnc(pref + line);
}
}
info(message) {
this.__write(message, "INFO", console.info);
info(message: string): void {
this.write(message, "INFO", console.info);
}
warn(message) {
this.__write(message, "WARN", console.warn);
warn(message: string): void {
this.write(message, "WARN", console.warn);
}
error(message, error) {
this.__write(message, "ERR!", console.error);
if (error !== undefined)
this.__write(error.stack, "ERR!", console.error);
error(message: string, error: Error): void {
this.write(message, "ERR!", console.error);
if (error !== undefined && error.stack !== undefined)
this.write(error.stack, "ERR!", console.error);
}
fatal(message, error, code) {
this.__write(message, "FTAL", console.error);