diff --git a/src/Client.ts b/src/Client.ts index 4e6a11e..440e653 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,7 +1,6 @@ -/* eslint-disable no-console */ - import Eris from 'eris'; import mongoose from 'mongoose'; +import signale from 'signale'; import fs from 'fs-extra'; import path from 'path'; import { config, Util } from '.'; @@ -11,6 +10,8 @@ import { Command } from './class'; export default class Client extends Eris.Client { + public config: { 'token': string; 'cloudflare': string; 'prefix': string; }; + public util: Util; public commands: Map; @@ -21,14 +22,23 @@ export default class Client extends Eris.Client { public stores: { emojis: { success: string, loading: string, error: string }; }; + public signale: signale.Signale; + constructor() { super(config.token, { getAllUsers: true, restMode: true, defaultImageFormat: 'png' }); + this.config = config; this.util = new Util(this); this.commands = new Map(); this.aliases = new Map(); this.db = { Account, Domain, Moderation }; this.stores = { emojis }; + this.signale = signale; + this.signale.config({ + displayDate: true, + displayTimestamp: true, + displayFilename: true, + }); } public loadCommand(commandPath: string) { @@ -36,24 +46,21 @@ export default class Client extends Eris.Client { try { const command = new (require(commandPath))(this); this.commands.set(command.name, command); - return `Successfully loaded ${command.name}.`; + this.signale.complete(`Loaded command ${command.name}`); } catch (err) { throw err; } } public async init() { const evtFiles = await fs.readdir('./events/'); - const commands = await fs.readdir(path.join(__dirname, './commands/')); commands.forEach((command) => { - const response = this.loadCommand(`./commands/${command}`); - if (response) console.log(response); + this.loadCommand(`./commands/${command}`); }); - console.log(`Loading a total of ${evtFiles.length} events.`); evtFiles.forEach((file) => { const eventName = file.split('.')[0]; - console.log(`Loading Event: ${eventName}`); const event = new (require(`./events/${file}`))(this); + this.signale.complete(`Loaded event ${eventName}`); this.on(eventName, (...args) => event.run(...args)); delete require.cache[require.resolve(`./events/${file}`)]; }); diff --git a/src/commands/cwg.ts b/src/commands/cwg.ts new file mode 100644 index 0000000..00904b4 --- /dev/null +++ b/src/commands/cwg.ts @@ -0,0 +1,92 @@ +import fs from 'fs-extra'; +import axios from 'axios'; +import x509 from '@ghaiklor/x509'; +import { Message } from 'eris'; +import { AccountInterface } from '../models'; +import { Command, RichEmbed } from '../class'; +import Client from '../Client'; + +export default class CWG extends Command { + constructor(client: Client) { + super(client); + this.name = 'cwg'; + this.description = 'Manages aspects for the CWG.'; + this.permissions = { roles: ['525441307037007902'] }; + this.enabled = true; + } + + public async run(message: Message, args?: string[]) { + /* + args[1] should be the user's ID OR account username; required + args[2] should be the domain; required + args[3] should be the port; required + args[4] should be the path to the x509 certificate; not required + args[5] should be the path to the x509 key; not required + */ + if (args[0] === 'create') { + const account = await this.client.db.Account.findOne({ $or: [{ account: args[1] }, { userID: args[1] }] }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} Cannot locate account, please try again.`); + try { + const domain = await this.createDomain(account, args[2], Number(args[3]), { cert: args[4], key: args[5] }); + const embed = new RichEmbed(); + embed.setTitle('Domain Creation'); + embed.setColor(3066993); + embed.addField('Account Username', account.account, true); + embed.addField('Account ID', account.id, true); + embed.addField('Engineer', `<@${message.author.id}>`, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + const cert = x509.parseCert(await fs.readFile(domain.x509.cert, { encoding: 'utf8' })); + embed.addField('Certificate Issuer', cert.issuer.organizationName, true); + embed.addField('Certificate Subject', cert.subject.commonName, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + // @ts-ignore + message.channel.createMessage({ embed }); + } catch (err) { + this.client.util.handleError(err, message, this); + } + } + return true; + } + + /** + * This function binds a domain to a port on the CWG. + * @param account The account of the user. + * @param subdomain The domain to use. `mydomain.cloud.libraryofcode.org` + * @param port The port to use, must be between 1024 and 65535. + * @param x509 The paths to the certificate and key files. Must be already existant. + * @example await CWG.createDomain('mydomain.cloud.libraryofcode.org', 6781); + */ + public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert: string, key: string } = { cert: '/etc/nginx/ssl/cloud-org.chain.crt', key: '/etc/nginx/ssl/cloud-org.key.pem' }) { + if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); + if (await this.client.db.Domain.exists({ port })) throw new Error(`Port ${port} already exists in the database.`); + if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); + if (!await this.client.db.Account.exists({ userID: account.userID })) throw new Error(`Cannot find account ${account.id}.`); + await fs.access(x509Certificate.cert, fs.constants.R_OK); + await fs.access(x509Certificate.key, fs.constants.R_OK); + let config = await fs.readFile('./static/nginx.conf', { encoding: 'utf8' }); + config = config.replace(/\[DOMAIN]/g, domain); + config = config.replace(/\[PORT]/g, String(port)); + config = config.replace(/\[CERTIFICATE]/g, x509Certificate.cert); + config = config.replace(/\[KEY]/g, x509Certificate.key); + await fs.writeFile(`/etc/nginx/sites-available/${domain}`, config, { encoding: 'utf8' }); + await fs.symlink(`/etc/nginx/sites-available/${domain}`, `/etc/nginx/sites-enabled/${domain}`); + const entry = new this.client.db.Domain({ + account, + domain, + port, + x509, + enabled: true, + }); + if (domain.includes('cloud.libraryofcode.org')) { + const method = await axios({ + method: 'post', + url: 'https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records', + headers: { Authorization: `Bearer ${this.client.config.cloudflare}`, 'Content-Type': 'application/json' }, + data: JSON.stringify({ type: 'CNAME', name: domain, content: 'cloud.libraryofcode.org', proxied: false }), + }); + } + return entry.save(); + } +} diff --git a/types/x509.d.ts b/types/x509.d.ts new file mode 100644 index 0000000..5704063 --- /dev/null +++ b/types/x509.d.ts @@ -0,0 +1,50 @@ +declare module '@ghaiklor/x509' { + namespace Certificate { + interface Issuer { + countryName: string, + stateOrProvinceName: string, + localityName: string, + organizationName: string, + commonName: string + } + interface Subject { + countryName: string, + postalCode: string, + stateOrProvinceName: string, + localityName: string, + streetAddress: string, + organizationName: string, + organizationalUnitName: string, + commonName: string + } + interface Extensions { + keyUsage: string, + authorityInformationAccess: string, + certificatePolicies: string, + basicConstraints: string, + cRLDistributionPoints: string, + subjectAlternativeName: string, + extendedKeyUsage: string, + authorityKeyIdentifier: string, + subjectKeyIdentifier: string, + cTPrecertificateSCTs: string + } + } + interface FullCertificate { + version: number, + subject: Certificate.Subject, + issuer: Certificate.Issuer, + serial: number, + notBefore: Date, + notAfter: Date, + subjectHash: string, + signatureAlgorithm: string, + publicKey: { algorithm: string }; + altNames: string[] + extensions: Certificate.Extensions + } + function getAltNames(cert: string): string[]; + function getIssuer(cert: string): Certificate.Issuer; + function getSubject(cert: string): Certificate.Subject; + function parseCert(cert: string): FullCertificate +}