diff --git a/src/class/Client.ts b/src/class/Client.ts new file mode 100644 index 0000000..5ca7265 --- /dev/null +++ b/src/class/Client.ts @@ -0,0 +1,135 @@ +import Eris from 'eris'; +import Redis from 'ioredis'; +import mongoose from 'mongoose'; +import signale from 'signale'; +import fs from 'fs-extra'; +import config from '../config.json'; +import CSCLI from '../cscli/main'; +import { Server } from '../api'; +import { Account, AccountInterface, Moderation, ModerationInterface, Domain, DomainInterface, Tier, TierInterface } from '../models'; +import { emojis } from '../stores'; +import { Command, Util, Collection } from '.'; +import * as commands from '../commands'; + + +export default class Client extends Eris.Client { + public config: { 'token': string; 'cloudflare': string; 'prefix': string; 'emailPass': string; 'mongoURL': string; 'port': number; 'keyPair': { 'publicKey': string; 'privateKey': string; }; }; + + public util: Util; + + public commands: Collection; + + public db: { Account: mongoose.Model; Domain: mongoose.Model; Moderation: mongoose.Model; Tier: mongoose.Model; }; + + public redis: Redis.Redis; + + public stores: { emojis: { success: string, loading: string, error: string }; }; + + public functions: Collection; + + public signale: signale.Signale; + + public server: Server; + + public updating: boolean; + + public buildError: boolean + + constructor() { + super(config.token, { getAllUsers: true, restMode: true, defaultImageFormat: 'png' }); + + process.title = 'cloudservices'; + this.config = config; + this.util = new Util(this); + this.commands = new Collection(); + this.functions = new Collection(); + this.db = { Account, Domain, Moderation, Tier }; + this.redis = new Redis(); + this.stores = { emojis }; + this.signale = signale; + this.signale.config({ + displayDate: true, + displayTimestamp: true, + displayFilename: true, + }); + this.updating = false; + this.buildError = false; + this.events(); + this.loadFunctions(); + this.init(); + } + + private async events() { + process.on('unhandledRejection', (error) => { + this.signale.error(error); + }); + } + + private async loadFunctions() { + const functions = await fs.readdir('./functions'); + functions.forEach(async (func) => { + if (func === 'index.ts' || func === 'index.js') return; + try { + const funcRequire: Function = require(`./functions/${func}`).default; + this.functions.set(func.split('.')[0], funcRequire); + } catch (error) { + this.signale.error(`Error occured loading ${func}`); + await this.util.handleError(error); + } + }); + } + + public loadCommand(CommandFile: any) { + // eslint-disable-next-line no-useless-catch + try { + // eslint-disable-next-line + const command: Command = new CommandFile(this); + if (command.subcmds.length) { + command.subcmds.forEach((C) => { + const cmd: Command = new C(this); + command.subcommands.add(cmd.name, cmd); + }); + } + delete command.subcmds; + this.commands.add(command.name, command); + this.signale.complete(`Loaded command ${command.name}`); + } catch (err) { throw err; } + } + + public async init() { + const evtFiles = await fs.readdir('./events/'); + Object.values(commands).forEach((c: Function) => this.loadCommand(c)); + + evtFiles.forEach((file) => { + const eventName = file.split('.')[0]; + if (file === 'index.js') return; + // eslint-disable-next-line + const event = new (require(`./events/${file}`).default)(this); + this.signale.complete(`Loaded event ${eventName}`); + this.on(eventName, (...args) => event.run(...args)); + delete require.cache[require.resolve(`./events/${file}`)]; + }); + + await mongoose.connect(config.mongoURL, { useNewUrlParser: true, useUnifiedTopology: true }); + await this.connect(); + this.on('ready', () => { + this.signale.info(`Connected to Discord as ${this.user.username}#${this.user.discriminator}`); + }); + const intervals = await fs.readdir('./intervals'); + intervals.forEach((interval) => { + // eslint-disable-next-line + if (interval === 'index.js') return; + require(`./intervals/${interval}`).default(this); + this.signale.complete(`Loaded interval ${interval.split('.')[0]}`); + }); + this.server = new Server(this, { port: this.config.port }); + // eslint-disable-next-line no-new + new CSCLI(this); + + const corepath = '/opt/CloudServices/dist'; + const cmdFiles = await fs.readdir('/opt/CloudServices/dist/commands'); + cmdFiles.forEach((f) => delete require.cache[`${corepath}/${f}`]); + delete require.cache[`${corepath}/config.json`]; + delete require.cache[`${corepath}/class/Util`]; + } +} diff --git a/src/class/Security.ts b/src/class/Security.ts new file mode 100644 index 0000000..5488662 --- /dev/null +++ b/src/class/Security.ts @@ -0,0 +1,77 @@ +/* eslint-disable no-underscore-dangle */ +import crypto from 'crypto'; +import { Request } from 'express'; +import { Client } from '..'; +import { AccountInterface } from '../models'; + +export default class Security { + public client: Client; + + protected readonly keyBase: { key: string, iv: string }; + + constructor(client: Client) { + this.client = client; + this.keyBase = require(`${process.cwd()}/keys.json`); + } + + get keys() { + return { + key: Buffer.from(this.keyBase.key, 'base64'), + iv: Buffer.from(this.keyBase.iv, 'base64'), + }; + } + + /** + * Creates a new Bearer token. + * @param _id The Mongoose Document property labeled ._id + */ + public async createBearer(_id: string): Promise { + let account = await this.client.db.Account.findOne({ _id }); + if (!account) throw new Error(`Account [${_id}] cannot be found.`); + const salt = crypto.randomBytes(50).toString('base64'); + const cipher = crypto.createCipheriv('aes-256-gcm', this.keys.key, this.keys.iv); + await account.updateOne({ salt }); + account = await this.client.db.Account.findOne({ _id }); + let encrypted = cipher.update(JSON.stringify(account), 'utf8', 'base64'); + encrypted += cipher.final('base64'); + await account.updateOne({ authTag: cipher.getAuthTag() }); + 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 { + const salt = bearer.split(':')[0]; + const saltCheck = await this.client.db.Account.findOne({ salt }); + const encrypted = bearer.split(':')[1]; + let decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decipher.setAuthTag(saltCheck.authTag); + decrypted += decipher.final('utf8'); + const json = JSON.parse(decrypted); + const account = await this.client.db.Account.findOne({ username: json.username }); + if (saltCheck.salt !== account.salt) return null; + return account; + } catch (error) { + this.client.util.handleError(error); + return null; + } + } + + /** + * 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]; + } + if (req.query && req.query.token) { + return req.query.token as string; + } + return '0000000000'; + } +} diff --git a/src/class/Server.ts b/src/class/Server.ts new file mode 100644 index 0000000..ded4c5f --- /dev/null +++ b/src/class/Server.ts @@ -0,0 +1,70 @@ +/* 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 '../api'; +import { Collection, Route } from '.'; + +export default class Server { + public routes: Collection + + public client: Client; + + public security: Security; + + public app: express.Express; + + public options: { port: number } + + constructor(client: Client, options?: { port: number }) { + this.options = options; + this.routes = new Collection(); + this.client = client; + this.security = new Security(this.client); + this.app = express(); + this.connect(); + this.loadRoutes(); + } + + private async loadRoutes(): Promise { + const routes = await fs.readdir(`${__dirname}/routes`); + routes.forEach(async (routeFile) => { + if (routeFile === 'index.js') return; + try { + // eslint-disable-next-line new-cap + const route: Route = new (require(`${__dirname}/routes/${routeFile}`).default)(this); + 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}`); + } catch (error) { + this.client.util.handleError(error); + } + }); + } + + 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/class/index.ts b/src/class/index.ts index a2744c7..c40f8ca 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -1,6 +1,7 @@ export { default as AccountUtil } from './AccountUtil'; +export { default as Client } from './Client'; +export { default as Collection } from './Collection'; export { default as Command } from './Command'; export { default as RichEmbed } from './RichEmbed'; -export { default as Util } from './Util'; -export { default as Collection } from './Collection'; export { default as Route } from './Route'; +export { default as Util } from './Util';