diff --git a/src/Client.ts b/src/Client.ts index 236897d..e8638a4 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -4,6 +4,7 @@ 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'; @@ -122,6 +123,8 @@ export default class Client extends Eris.Client { 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'); diff --git a/src/class/AccountUtil.ts b/src/class/AccountUtil.ts index 43816e8..01b004d 100644 --- a/src/class/AccountUtil.ts +++ b/src/class/AccountUtil.ts @@ -1,3 +1,4 @@ +import moment from 'moment'; import { randomBytes } from 'crypto'; import { AccountInterface } from '../models'; import { Client } from '..'; @@ -75,4 +76,30 @@ export default class AccountUtil { + `Your support key is \`${code}\`. Pin this message, you may need this key to contact Library of Code in the future.`).catch(); return { account: accountInterface, tempPass }; } + + public async lock(username: string, moderatorID: string, data?: { reason?: string, time?: number}) { + const account = await this.client.db.Account.findOne({ username }); + if (!account) throw new Error('Account does not exist.'); + if (account.locked) throw new Error('Account is already locked.'); + if (account.username === 'matthew' || account.root) throw new Error('Permission denied.'); + await this.client.util.exec(`lock ${account.username}`); + await account.updateOne({ locked: true }); + + await this.client.util.createModerationLog(account.userID, this.client.users.get(moderatorID), 2, data?.reason, data?.time); + + this.client.util.transport.sendMail({ + to: account.emailAddress, + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Your account has been locked', + html: ` +

Library of Code | Cloud Services

+

Your Cloud Account has been locked until ${data?.time ? moment(data?.time).calendar() : 'indefinitely'} under the EULA.

+

Reason: ${data?.reason ? data.reason : 'none provided'}

+

Technician: ${moderatorID !== this.client.user.id ? this.client.users.get(moderatorID).username : 'SYSTEM'}

+

Expiration: ${data?.time ? moment(data?.time).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'N/A'}

+ + Library of Code sp-us | Support Team + `, + }); + } } diff --git a/src/cscli/main.ts b/src/cscli/main.ts new file mode 100644 index 0000000..eaa0134 --- /dev/null +++ b/src/cscli/main.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-case-declarations */ +/* eslint-disable consistent-return */ +import net from 'net'; +import crypto from 'crypto'; +import { promises as fs } from 'fs'; +import Client from '../Client'; + +export default class CSCLI { + public client: Client; + + public server: net.Server; + + #hmac: string; + + constructor(client: Client) { + this.client = client; + this.loadKeys(); + this.server = net.createServer((socket) => { + socket.on('data', async (data) => { + try { + await this.handle(socket, data); + } catch (err) { + await this.client.util.handleError(err); + socket.destroy(); + } + }); + }); + this.init(); + } + + public async handle(socket: net.Socket, data: Buffer) { + const parsed: { Username: string, Type: string, Message?: string, HMAC: string } = JSON.parse(data.toString().trim()); + let verificationParsed: any = parsed; + delete verificationParsed.HMAC; + verificationParsed = JSON.stringify(verificationParsed); + const verification = this.verifyConnection(parsed.HMAC, verificationParsed); + if (!verification) return socket.destroy(); + // FINISH VERIFICATION CHECKS + switch (parsed.Type) { + case 'lock': + await this.client.util.accounts.lock(parsed.Username, this.client.user.id, { reason: 'Failed to accept Terms of Service.' }); + break; + case 'ram': + const mem = Number(await this.client.util.exec(`memory ${parsed.Username}`)) * 1000; + const memoryConversion = mem / 1024 / 1024; + socket.write(`${memoryConversion}\n`); + socket.destroy(); + break; + case 'storage': + const res = await this.client.redis.get(`storage-${parsed.Username}`) ? Number(await this.client.redis.get(`storage-${parsed.Username}`)) : '0'; + socket.write(`${res}\n`); + socket.destroy(); + break; + default: + socket.destroy(); + break; + } + } + + public verifyConnection(key: string, data: any): boolean { + const hmac = crypto.createHmac('sha256', this.#hmac); + hmac.update(data); + const computed = hmac.digest('hex'); + if (computed === key) return true; + return false; + } + + public async loadKeys() { + const key = await fs.readFile('/etc/cscli.conf', { encoding: 'utf8' }); + this.#hmac = key.toString().trim(); + } + + public init() { + this.server.on('error', (err) => { + this.client.util.handleError(err); + }); + this.server.listen(8124, () => { + this.client.signale.success('TCP socket is now listening for connections.'); + }); + } +}