From ae5dbf5f48c40bdfea77e304ccf7f3b9e5d7deae Mon Sep 17 00:00:00 2001 From: Calvin Montgomery Date: Thu, 1 Sep 2022 20:17:21 -0700 Subject: [PATCH] Continue working on banned channels --- bin/admin.js | 93 +++++++++++++++++++++++++++++++ src/cli/banned-channels.js | 12 ++++ src/controller/banned-channels.js | 29 ++++++++++ src/database/channels.js | 22 ++++++++ src/main.js | 30 +++++++++- src/server.js | 6 ++ src/servsock.js | 2 +- 7 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 bin/admin.js create mode 100644 src/cli/banned-channels.js create mode 100644 src/controller/banned-channels.js diff --git a/bin/admin.js b/bin/admin.js new file mode 100644 index 00000000..8f9478a1 --- /dev/null +++ b/bin/admin.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node + +const Config = require('../lib/config'); +Config.load('config.yaml'); + +if (!Config.get('service-socket.enabled')){ + console.error('The Service Socket is not enabled.'); + process.exit(1); +} + +const net = require('net'); +const path = require('path'); +const readline = require('node:readline/promises'); + +const socketPath = path.resolve(__dirname, '..', Config.get('service-socket.socket')); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +async function doCommand(params) { + return new Promise((resolve, reject) => { + const client = net.createConnection(socketPath); + + client.on('connect', () => { + client.write(JSON.stringify(params) + '\n'); + }); + + client.on('data', data => { + client.end(); + resolve(JSON.parse(data)); + }); + + client.on('error', error => { + reject(error); + }); + }); +} + +let commands = [ + { + command: 'ban-channel', + handler: async args => { + if (args.length !== 3) { + console.log('Usage: ban-channel '); + process.exit(1); + } + + let [name, externalReason, internalReason] = args; + let answer = await rl.question(`Ban ${name} with external reason "${externalReason}" and internal reason "${internalReason}"? `); + + + if (/^[yY]$/.test(answer)) { + let res = await doCommand({ + command: 'ban-channel', + name, + externalReason, + internalReason + }); + + console.log(`Status: ${res.status}`); + if (res.status === 'error') { + console.log('Error:', res.error); + process.exit(1); + } else { + process.exit(0); + } + } else { + console.log('Aborted.'); + } + } + } +]; + +let found = false; +commands.forEach(cmd => { + if (cmd.command === process.argv[2]) { + found = true; + cmd.handler(process.argv.slice(3)).then(() => { + process.exit(0); + }).catch(error => { + console.log('Error in command:', error.stack); + }); + } +}); + +if (!found) { + console.log('Available commands:'); + commands.forEach(cmd => { + console.log(` * ${cmd.command}`); + }); + process.exit(1); +} diff --git a/src/cli/banned-channels.js b/src/cli/banned-channels.js new file mode 100644 index 00000000..06028e95 --- /dev/null +++ b/src/cli/banned-channels.js @@ -0,0 +1,12 @@ +import Server from '../server'; + +export async function handleBanChannel({ name, externalReason, internalReason }) { + await Server.getServer().bannedChannelsController.banChannel({ + name, + externalReason, + internalReason, + bannedBy: '[console]' + }); + + return { status: 'success' }; +} diff --git a/src/controller/banned-channels.js b/src/controller/banned-channels.js new file mode 100644 index 00000000..ff524b70 --- /dev/null +++ b/src/controller/banned-channels.js @@ -0,0 +1,29 @@ +const LOGGER = require('@calzoneman/jsli')('BannedChannelsController'); + +export class BannedChannelsController { + constructor(dbChannels, globalMessageBus) { + this.dbChannels = dbChannels; + this.globalMessageBus = globalMessageBus; + } + + async banChannel({ name, externalReason, internalReason, bannedBy }) { + LOGGER.info(`Banning channel ${name} (banned by ${bannedBy})`); + + let banInfo = await this.dbChannels.getBannedChannel(name); + if (banInfo !== null) { + LOGGER.warn(`Channel ${name} is already banned, updating ban reason`); + } + + await this.dbChannels.putBannedChannel({ + name, + externalReason, + internalReason, + bannedBy + }); + + this.globalMessageBus.emit( + 'ChannelBanned', + { channel: name, externalReason } + ); + } +} diff --git a/src/database/channels.js b/src/database/channels.js index 57d5c391..a78ceb40 100644 --- a/src/database/channels.js +++ b/src/database/channels.js @@ -2,6 +2,7 @@ var db = require("../database"); var valid = require("../utilities").isValidChannelName; var Flags = require("../flags"); var util = require("../utilities"); +// TODO: I think newer knex has native support for this import { createMySQLDuplicateKeyUpdate } from '../util/on-duplicate-key-update'; import Config from '../config'; @@ -746,5 +747,26 @@ module.exports = { updatedAt: rows[0].updated_at }; }); + }, + + putBannedChannel: async function putBannedChannel({ name, externalReason, internalReason, bannedBy }) { + if (!valid(name)) { + throw new Error("Invalid channel name"); + } + + return await db.getDB().runTransaction(async tx => { + let insert = tx.table('banned_channels') + .insert({ + channel_name: name, + external_reason: externalReason, + internal_reason: internalReason, + banned_by: bannedBy + }); + let update = tx.raw(createMySQLDuplicateKeyUpdate( + ['external_reason', 'internal_reason'] + )); + + return tx.raw(insert.toString() + update.toString()); + }); } }; diff --git a/src/main.js b/src/main.js index 325d2e49..6b240fbf 100644 --- a/src/main.js +++ b/src/main.js @@ -2,6 +2,7 @@ import Config from './config'; import * as Switches from './switches'; import { eventlog } from './logger'; require('source-map-support').install(); +import * as bannedChannels from './cli/banned-channels'; const LOGGER = require('@calzoneman/jsli')('main'); @@ -28,10 +29,33 @@ if (!Config.get('debug')) { }); } +async function handleCliCmd(cmd) { + try { + switch (cmd.command) { + case 'ban-channel': + return await bannedChannels.handleBanChannel(cmd); + default: + throw new Error(`Unrecognized command "${cmd.command}"`); + } + } catch (error) { + return { status: 'error', error: String(error) }; + } +} + // TODO: this can probably just be part of servsock.js // servsock should also be refactored to send replies instead of // relying solely on tailing logs -function handleLine(line) { +function handleLine(line, client) { + try { + let cmd = JSON.parse(line); + handleCliCmd(cmd).then(res => { + client.write(JSON.stringify(res) + '\n'); + }).catch(error => { + LOGGER.error(`Unexpected error in handleCliCmd: ${error.stack}`); + }); + } catch (_error) { + } + if (line === '/reload') { LOGGER.info('Reloading config'); try { @@ -81,9 +105,9 @@ if (Config.get('service-socket.enabled')) { const ServiceSocket = require('./servsock'); const sock = new ServiceSocket(); sock.init( - line => { + (line, client) => { try { - handleLine(line); + handleLine(line, client); } catch (error) { LOGGER.error( 'Error in UNIX socket command handler: %s', diff --git a/src/server.js b/src/server.js index 7b7eafdc..6921f504 100644 --- a/src/server.js +++ b/src/server.js @@ -48,6 +48,7 @@ import { PartitionModule } from './partition/partitionmodule'; import { Gauge } from 'prom-client'; import { EmailController } from './controller/email'; import { CaptchaController } from './controller/captcha'; +import { BannedChannelsController } from './controller/banned-channels'; var Server = function () { var self = this; @@ -109,6 +110,11 @@ var Server = function () { Config.getCaptchaConfig() ); + self.bannedChannelsController = new BannedChannelsController( + self.db.channels, + globalMessageBus + ); + // webserver init ----------------------------------------------------- const ioConfig = IOConfiguration.fromOldConfig(Config); const webConfig = WebConfiguration.fromOldConfig(Config); diff --git a/src/servsock.js b/src/servsock.js index 4597c96e..d5870c43 100644 --- a/src/servsock.js +++ b/src/servsock.js @@ -34,7 +34,7 @@ export default class ServiceSocket { delete this.connections[id]; }); stream.on('data', (msg) => { - this.handler(msg.toString()); + this.handler(msg.toString(), stream); }); }).listen(this.socket); process.on('exit', this.closeServiceSocket.bind(this));