This commit is contained in:
Erik 2022-11-09 11:24:12 +02:00
parent 1b7cf44154
commit f5e4c11b99
Signed by: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB

164
src/server/Server.js Normal file
View File

@ -0,0 +1,164 @@
// Native
const { EventEmitter } = require('node:events');
// const { inspect } = require('node:util');
const path = require('node:path');
const http = require('node:http2');
// External
const { LoggerClient } = require('@navy.gif/logger');
// const { Collection } = require('@discordjs/collection');
const express = require('express');
const { default: helmet } = require('helmet');
// Local
const { Util } = require('../util');
const { Registry, UserDatabase } = require('./components');
const { MariaDB, MongoDB } = require('./database');
const Authenticator = require('./middleware/Authenticator');
/**
* Meant to be deployed behind a proxy (e.g. nginx) instance that takes care of certs and load balancing, trusts the first proxy
*
* @class Server
* @extends {EventEmitter}
*/
class Server extends EventEmitter {
constructor (options = {}) {
super();
const { MARIA_HOST, MARIA_USER, MARIA_PORT, MARIA_PASS, MARIA_DB,
MONGO_HOST, MONGO_USER, MONGO_PORT, MONGO_PASS, MONGO_DB,
NODE_ENV, SECRET, DISCORD_SECRET } = process.env; // Secret used for cookies, discord secret is for discord oauth
const { discord, http: httpOpts, databases } = options;
this._options = options;
this._shardId = parseInt(process.env.SHARD_ID);
this._ready = false;
this._debug = options.debug || false;
this._proto = NODE_ENV === 'development' ? 'http' : 'https';
if (!httpOpts?.port) return Util.fatal(new Error(`Missing http.port in server options`));
this.port = httpOpts.port + this._shardId;
this.domain = options.domain;
this.server = null;
this.app = express();
this.registry = new Registry(this, { path: path.join(__dirname, 'endpoints') });
this.logger = new LoggerClient({ ...options.logger, name: this.constructor.name });
this.mariadb = new MariaDB(this, { options: databases.mariadb, MARIA_HOST, MARIA_USER, MARIA_PORT, MARIA_PASS, MARIA_DB });
this.mongodb = new MongoDB(this, { options: databases.mongodb, MONGO_HOST, MONGO_USER, MONGO_PORT, MONGO_PASS, MONGO_DB });
this.userDatabase = new UserDatabase(this.mongodb);
this.authenticator = new Authenticator(this, this.app, this.userDatabase, {
mongo: this.mongodb.client,
secret: SECRET,
discordID: discord.id,
discordScope: discord.scope,
discordSecret: DISCORD_SECRET,
discordVersion: discord.version,
callbackURL: `${this.baseURL}${discord.callbackURL}`,
cookie: {
secure: true
}
});
// Expecting every other env to be behind a proxy
if (NODE_ENV !== 'development') this.app.set('trust proxy', 1);
this.app.use(helmet());
this.app.use(express.json({ limit: '10mb' }));
this.app.use(this.logRequest.bind(this)); // Logs every request
this.app.use(this.logError.bind(this)); // Logs endpoints that error and sends a 500
this.app.use(this.ready.bind(this)); // denies requests before the server is ready
process.on('message', this._handleMessage.bind(this));
}
async init () {
const start = Date.now();
this.logger.status('Starting server');
this.logger.info('Initialising MariaDB');
this.mariadb.init();
this.logger.info('Initialising MongoDB');
await this.mongodb.init();
this.userDatabase.init();
this.logger.info('Loading endpoints');
this.registry.loadEndpoints();
this.logger.debug(this.registry.print);
this.logger.info('Creating http server');
this.server = http.createServer(this._options.http, this.app).listen(this.port);
this._ready = true;
this.logger.status(`Server created, took ${Date.now() - start} ms, listening on port ${this.port}`);
process.send({ _ready: true });
}
// eslint-disable-next-line no-unused-vars
logError (err, req, res, next) {
this.logger.error(`Unhandled error:\n${err.stack || err}`);
res.status(500).send('An internal error was encountered');
}
logRequest (req, res, next) {
res.once('finish', () => {
this.logger[res.statusCode === 401 ? 'unauthorised' : 'access'](`[${req.get('X-Forwarded-For') || req.socket.remoteAddress}] [STATUS: ${res.statusCode}] Request to ${req.route?.path || req.path}`);
});
next();
}
ready (req, res, next) {
if (!this._ready) return res.status(503).send('Server not ready');
next();
}
async shutdown () {
this.logger.info(`Received shutdown command, initiating graceful shutdown`);
process.send({ _shutdown: true });
this._ready = false; // stops any new connections
await this.mongodb.close();
await this.mariadb.close();
this.logger.status('DB shutdowns complete.');
this.logger.status('Shutdown complete. Goodbye');
// eslint-disable-next-line no-process-exit
process.exit();
}
_handleMessage (msg) {
if (msg._shutdown) this.shutdown();
}
createLogger (comp) {
return new LoggerClient({ name: comp.constructor.name, ...this._options.logger });
}
get baseURL () {
return `${this._proto}://${this.domain}/`;
}
}
process.once('message', (msg) => {
if (msg._start) {
const options = msg._start;
const server = new Server(options);
server.init();
}
});
module.exports = Server;