diff --git a/.eslintrc.json b/.eslintrc.json index 3a31097..f1f15b9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -36,6 +36,8 @@ "camelcase": "off", "indent": "warn", "object-curly-newline": "off", - "import/prefer-default-export": "off" + "import/prefer-default-export": "off", + "no-useless-constructor": "off", + "@typescript-eslint/no-useless-constructor": 2 } } \ No newline at end of file diff --git a/package.json b/package.json index a6f520d..ad00d84 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "eris-pagination": "bsian03/eris-pagination", "express": "^4.17.1", "fs-extra": "^8.1.0", + "helmet": "^3.21.2", "ioredis": "^4.14.1", "moment": "^2.24.0", "moment-precise-range-plugin": "^1.3.0", @@ -29,6 +30,7 @@ "devDependencies": { "@types/express": "^4.17.2", "@types/fs-extra": "^8.0.0", + "@types/helmet": "^0.0.45", "@types/ioredis": "^4.0.18", "@types/mongoose": "^5.5.20", "@types/nodemailer": "^6.2.1", diff --git a/src/api/Security.ts b/src/api/Security.ts index a59c64b..2058619 100644 --- a/src/api/Security.ts +++ b/src/api/Security.ts @@ -38,6 +38,10 @@ export default class Security { return `${salt}:${encrypted}`; } + /** + * If the bearer token is valid, will return the Account, else will return null. + * @param bearer The bearer token provided. + */ public async checkBearer(bearer: string): Promise { const decipher = crypto.createDecipheriv('aes-256-gcm', this.keys.key, this.keys.iv); try { @@ -57,6 +61,10 @@ export default class Security { } } + /** + * Returns the Bearer token, searches in headers and query. + * @param req The Request object from Express. + */ public extractBearer(req: Request): string { if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { return req.headers.authorization.split(' ')[1]; diff --git a/src/api/Server.ts b/src/api/Server.ts index 43f77a3..13d2603 100644 --- a/src/api/Server.ts +++ b/src/api/Server.ts @@ -1,13 +1,14 @@ /* eslint-disable no-useless-return */ import express from 'express'; import bodyParser from 'body-parser'; +import helmet from 'helmet'; import fs from 'fs-extra'; import { Client } from '..'; import { Security } from '.'; -import { Route } from '../class'; +import { Collection, Route } from '../class'; export default class Server { - public routes: Map; + public routes: Collection public client: Client; @@ -19,7 +20,7 @@ export default class Server { constructor(client: Client, options?: { port: number }) { this.options = options; - this.routes = new Map(); + this.routes = new Collection(); this.client = client; this.security = new Security(this.client); this.app = express(); @@ -34,7 +35,13 @@ export default class Server { try { // eslint-disable-next-line new-cap const route = new (require(`${__dirname}/routes/${routeFile}`).default)(this); - route.bind(); + if (route.conf.deprecated === true) { + route.deprecated(); + } else if (route.conf.maintenance === true) { + route.maintenance(); + } else { + route.bind(); + } this.routes.set(route.conf.path, route); this.app.use(route.conf.path, route.router); this.client.signale.success(`Successfully loaded route ${route.conf.path}`); @@ -45,6 +52,16 @@ export default class Server { } private connect(): void { + this.app.set('trust proxy', 'loopback'); + this.app.use(helmet({ + hsts: false, + hidePoweredBy: false, + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + }, + }, + })); this.app.use(bodyParser.json()); this.app.listen(this.options.port, () => { this.client.signale.success(`API Server listening on port ${this.options.port}`); diff --git a/src/api/routes/Account.ts b/src/api/routes/Account.ts index 284f3d4..198bebd 100644 --- a/src/api/routes/Account.ts +++ b/src/api/routes/Account.ts @@ -10,33 +10,47 @@ export default class Account extends Route { public bind() { this.router.use(async (req, res, next) => { - const account = await this.server.security.checkBearer(this.server.security.extractBearer(req)); - if (!account) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: 'BEARER_TOKEN_INVALID' }); - Object.defineProperty(req, 'account', { value: account, writable: true, enumerable: true, configurable: true }); - next(); + await this.authorize(req, res, next); }); this.router.get('/', async (req: Req, res) => { - const acc: any = {}; - acc.username = req.account.username; - acc.userID = req.account.userID; - acc.email = req.account.emailAddress; - acc.locked = req.account.locked; - acc.root = req.account.root; - acc.createdAt = req.account.createdAt; - acc.createdBy = req.account.createdBy; - acc.permissions = req.account.permissions; - res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc }); + try { + const acc: any = {}; + acc.username = req.account.username; + acc.userID = req.account.userID; + acc.email = req.account.emailAddress; + acc.locked = req.account.locked; + acc.root = req.account.root; + acc.createdAt = req.account.createdAt; + acc.createdBy = req.account.createdBy; + acc.permissions = req.account.permissions; + res.status(200).json({ code: this.constants.codes.SUCCESS, message: acc }); + } catch (error) { + this.handleError(error, res); + } }); this.router.get('/moderations/:id?', async (req: Req, res) => { - const moderations = await this.server.client.db.Moderation.find({ username: req.account.username }); - if (!moderations.length) res.sendStatus(204); - if (req.params.id) { - const filtered = moderations.filter((moderation) => moderation.logID === req.params.id); - res.status(200).json({ code: this.constants.codes.SUCCESS, message: { filtered } }); - } else { - res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations }); + try { + const moderations = await this.server.client.db.Moderation.find({ username: req.account.username }); + if (!moderations.length) res.sendStatus(204); + if (req.params.id) { + const filtered = moderations.filter((moderation) => moderation.logID === req.params.id); + res.status(200).json({ code: this.constants.codes.SUCCESS, message: { filtered } }); + } else { + res.status(200).json({ code: this.constants.codes.SUCCESS, message: moderations }); + } + } catch (error) { + this.handleError(error, res); + } + }); + + this.router.get('/storage', async (req: Req, res) => { + try { + const data = await this.server.client.redis.get(`storage-${req.account.username}`) ? Number(await this.server.client.redis.get(`storage-${req.account.username}`)) : null; + res.status(200).json({ code: this.constants.codes.SUCCESS, message: data }); + } catch (error) { + this.handleError(error, res); } }); } diff --git a/src/api/routes/FileSystem.ts b/src/api/routes/FileSystem.ts new file mode 100644 index 0000000..f5f11ec --- /dev/null +++ b/src/api/routes/FileSystem.ts @@ -0,0 +1,17 @@ +/* eslint-disable consistent-return */ +import { Server } from '..'; +import { Route } from '../../class'; + +export default class FileSystem extends Route { + constructor(server: Server) { + super(server, { path: '/fs', deprecated: false, maintenance: true }); + } + + public bind() { + this.router.use(async (req, res, next) => { + await this.authorize(req, res, next); + }); + + this.router.get('/:'); + } +} diff --git a/src/api/routes/Root.ts b/src/api/routes/Root.ts new file mode 100644 index 0000000..4832b99 --- /dev/null +++ b/src/api/routes/Root.ts @@ -0,0 +1,41 @@ +import os from 'os'; +import { Server } from '..'; +import { Route } from '../../class'; + +export default class Root extends Route { + constructor(server: Server) { + super(server, { path: '/', deprecated: false }); + } + + public bind() { + this.router.get('/', async (req, res) => { + try { + const date = new Date(); + date.setSeconds(-process.uptime()); + const accounts = await this.server.client.db.Account.find(); + const administrators = accounts.filter((account) => account.root === true).length; + const response = { + nodeVersion: process.version, + uptime: process.uptime(), + server: { + users: accounts.length, + administrators, + }, + stats: { + uptime: os.uptime(), + loadAverage: os.loadavg(), + cpuModel: os.cpus()[0].model, + cpuClock: os.cpus()[0].speed / 1000, + cpuCores: os.cpus().length, + hostname: os.hostname(), + ipv4: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv4')[0].address, + ipv6: os.networkInterfaces().eth0.filter((r) => r.family === 'IPv6')[0].address, + }, + }; + res.status(200).json({ code: this.constants.codes.SUCCESS, message: response }); + } catch (error) { + this.handleError(error, res); + } + }); + } +} diff --git a/src/class/Route.ts b/src/class/Route.ts index 1fbc728..33db377 100644 --- a/src/class/Route.ts +++ b/src/class/Route.ts @@ -1,4 +1,5 @@ -import { Router as router } from 'express'; +/* eslint-disable consistent-return */ +import { Request, Response, NextFunction, Router as router } from 'express'; import { Server } from '../api'; export default class Route { @@ -8,7 +9,7 @@ export default class Route { public conf: { path: string, deprecated?: boolean }; - constructor(server: Server, conf: { path: string, deprecated?: boolean }) { + constructor(server: Server, conf: { path: string, deprecated?: boolean, maintenance?: boolean }) { this.server = server; this.router = router(); this.conf = conf; @@ -16,6 +17,47 @@ export default class Route { public bind() {} + public deprecated(): void { + this.router.all('*', (_req, res) => { + res.status(501).json({ code: this.constants.codes.DEPRECATED, message: this.constants.messages.DEPRECATED }); + }); + } + + public maintenance(): void { + this.router.all('*', (_req, res) => { + res.status(503).json({ code: this.constants.codes.MAINTENANCE_OR_UNAVAILABLE, message: this.constants.messages.MAINTENANCE_OR_UNAVAILABLE }); + }); + } + + /** + * This function checks for the presense of a Bearer token with Security.extractBearer(), + * then it will attempt to validate it with Security.checkBearer(). + * If it can authenticate the request, it'll add a custom property on Request called + * `account`, which will hold an the bearer token's account owner. The account is of the + * type `AccountInterface`. + * @param req The Request object from Express. + * @param res The Response object from Express. + * @param next The NextFunction from Express. + * @example Security.authorize(req, res, next); + */ + public async authorize(req: Request, res: Response, next: NextFunction) { + const account = await this.server.security.checkBearer(this.server.security.extractBearer(req)); + if (!account) return res.status(401).json({ code: this.constants.codes.UNAUTHORIZED, message: this.constants.messages.UNAUTHORIZED }); + Object.defineProperty(req, 'account', { value: account, writable: true, enumerable: true, configurable: true }); + next(); + } + + /** + * This function calls Util.handleError() internally, however it also sends a generic + * response to the user. + * @param error The Error object. + * @param res The Response object from Express. + */ + public handleError(error: Error, res: Response): void { + this.server.client.util.handleError(error); + res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: this.constants.messages.SERVER_ERROR }); + } + get constants() { return { codes: { @@ -26,7 +68,16 @@ export default class Route { ACCOUNT_NOT_FOUND: 1041, CLIENT_ERROR: 1044, SERVER_ERROR: 105, - UNKNOWN_SERVER_ERROR: 1051, + DEPRECATED: 1051, + MAINTENANCE_OR_UNAVAILABLE: 1053, + }, + messages: { + UNAUTHORIZED: ['CREDENTIALS_INVALID', 'The credentials you supplied are invalid.'], + PERMISSION_DENIED: ['PERMISSION_DENIED', 'You do not have valid credentials to access this resource.'], + NOT_FOUND: ['NOT_FOUND', 'The resource you requested cannot be located.'], + SERVER_ERROR: ['INTERNAL_ERROR', 'An internal error has occurred, Engineers have been notified.'], + DEPRECATED: ['ENDPOINT_OR_RESOURCE_DEPRECATED', 'The endpoint or resource you\'re trying to access has been deprecated.'], + MAINTENANCE_OR_UNAVAILABLE: ['SERVICE_UNAVAILABLE', 'The endpoint or resource you\'re trying to access is either in maintenance or is not available.'], }, }; } diff --git a/src/class/Util.ts b/src/class/Util.ts index 5fe2862..2197a92 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -43,8 +43,6 @@ export default class Util { */ public resolveCommand(command: string, args?: string[], message?: Message): Promise<{cmd: Command, args: string[] }> { try { - this.client.signale.info(command); - this.client.signale.info(args); let resolvedCommand: Command; if (this.client.commands.has(command)) resolvedCommand = this.client.commands.get(command); @@ -117,7 +115,7 @@ export default class Util { } } - public splitFields(fields: {name: string, value: string, inline?: boolean}[]): {name: string, value: string, inline?: boolean}[][] { + public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { let index = 0; const array: {name: string, value: string, inline?: boolean}[][] = [[]]; while (fields.length) { @@ -145,7 +143,7 @@ export default class Util { } - public async createHash(password: string) { + public async createHash(password: string): Promise { const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`); return hashed; } @@ -225,7 +223,7 @@ export default class Util { const expiration = { date, processed }; logInput.expiration = expiration; - const log = await new this.client.db.Moderation(logInput); + const log = new this.client.db.Moderation(logInput); await log.save(); let embedTitle: string; diff --git a/src/commands/cwg_create.ts b/src/commands/cwg_create.ts index 0dd4b7e..ef145c1 100644 --- a/src/commands/cwg_create.ts +++ b/src/commands/cwg_create.ts @@ -78,7 +78,7 @@ export default class CWG_Create extends Command { Port: ${domain.port}
Certificate Issuer: ${cert.issuer.organizationName}
Certificate Subject: ${cert.subject.commonName}
- Responsible Engineer: ${message.author.username}#${message.author.discriminator}
+ Responsible Engineer: ${message.author.username}#${message.author.discriminator}

If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.