Compare commits
7 commits
postgreSQL
...
banned-cha
Author | SHA1 | Date | |
---|---|---|---|
c3509b9639 | |||
7cf1593c7c | |||
c686100008 | |||
5985c8d280 | |||
d61af3f9d5 | |||
387faf6d75 | |||
d4727533b1 |
21
NEWS.md
21
NEWS.md
|
@ -1,3 +1,24 @@
|
|||
2022-09-21
|
||||
==========
|
||||
|
||||
**Upgrade intervention required**
|
||||
|
||||
This release adds a feature to ban channels, replacing the earlier (hastily
|
||||
added) configuration-based `channel-blacklist`. If you have any entries in
|
||||
`channel-blacklist` in your `config.yaml`, you will need to migrate them to the
|
||||
new bans table by using a command after upgrading (the ACP web interface hasn't
|
||||
been updated for this feature):
|
||||
|
||||
./bin/admin.js ban-channel <channel-name> <external-reason> <internal-reason>
|
||||
|
||||
The external reason will be displayed when users attempt to join the banned
|
||||
channel, while the internal reason is only displayed when using the
|
||||
`show-channel-ban` command.
|
||||
|
||||
You can later use `unban-channel` to remove a ban. The owner of the banned
|
||||
channel can still delete it, but the banned state will persist, so the channel
|
||||
cannot be re-registered later.
|
||||
|
||||
2022-08-28
|
||||
==========
|
||||
|
||||
|
|
176
bin/admin.js
Executable file
176
bin/admin.js
Executable file
|
@ -0,0 +1,176 @@
|
|||
#!/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 <name> <externalReason> <internalReason>');
|
||||
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)) {
|
||||
console.log('Aborted.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'ban-channel',
|
||||
name,
|
||||
externalReason,
|
||||
internalReason
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
console.log('Ban succeeded.');
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
command: 'unban-channel',
|
||||
handler: async args => {
|
||||
if (args.length !== 1) {
|
||||
console.log('Usage: unban-channel <name>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [name] = args;
|
||||
let answer = await rl.question(`Unban ${name}? `);
|
||||
|
||||
if (!/^[yY]$/.test(answer)) {
|
||||
console.log('Aborted.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'unban-channel',
|
||||
name
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
console.log('Unban succeeded.');
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
command: 'show-banned-channel',
|
||||
handler: async args => {
|
||||
if (args.length !== 1) {
|
||||
console.log('Usage: show-banned-channel <name>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let [name] = args;
|
||||
|
||||
let res = await doCommand({
|
||||
command: 'show-banned-channel',
|
||||
name
|
||||
});
|
||||
|
||||
switch (res.status) {
|
||||
case 'error':
|
||||
console.log('Error:', res.error);
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'success':
|
||||
if (res.ban != null) {
|
||||
console.log(`Channel: ${name}`);
|
||||
console.log(`Ban issued: ${res.ban.createdAt}`);
|
||||
console.log(`Banned by: ${res.ban.bannedBy}`);
|
||||
console.log(`External reason:\n${res.ban.externalReason}`);
|
||||
console.log(`Internal reason:\n${res.ban.internalReason}`);
|
||||
} else {
|
||||
console.log(`Channel ${name} is not banned.`);
|
||||
}
|
||||
process.exit(0);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown result: ${res.status}`);
|
||||
process.exit(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
109
integration_test/controller/banned-channels.js
Normal file
109
integration_test/controller/banned-channels.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
const assert = require('assert');
|
||||
const { BannedChannelsController } = require('../../lib/controller/banned-channels');
|
||||
const dbChannels = require('../../lib/database/channels');
|
||||
const testDB = require('../testutil/db').testDB;
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
require('../../lib/database').init(testDB);
|
||||
|
||||
const testBan = {
|
||||
name: 'ban_test_1',
|
||||
externalReason: 'because I said so',
|
||||
internalReason: 'illegal content',
|
||||
bannedBy: 'admin'
|
||||
};
|
||||
|
||||
async function cleanupTestBan() {
|
||||
return dbChannels.removeBannedChannel(testBan.name);
|
||||
}
|
||||
|
||||
describe('BannedChannelsController', () => {
|
||||
let controller;
|
||||
let messages;
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanupTestBan();
|
||||
messages = new EventEmitter();
|
||||
controller = new BannedChannelsController(
|
||||
dbChannels,
|
||||
messages
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupTestBan();
|
||||
});
|
||||
|
||||
it('bans a channel', async () => {
|
||||
assert.strictEqual(await controller.getBannedChannel(testBan.name), null);
|
||||
|
||||
let received = null;
|
||||
messages.once('ChannelBanned', cb => {
|
||||
received = cb;
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
let info = await controller.getBannedChannel(testBan.name);
|
||||
for (let field of Object.keys(testBan)) {
|
||||
// Consider renaming parameter to avoid this branch
|
||||
if (field === 'name') {
|
||||
assert.strictEqual(info.channelName, testBan.name);
|
||||
} else {
|
||||
assert.strictEqual(info[field], testBan[field]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(received, null);
|
||||
assert.strictEqual(received.channel, testBan.name);
|
||||
assert.strictEqual(received.externalReason, testBan.externalReason);
|
||||
});
|
||||
|
||||
it('updates an existing ban', async () => {
|
||||
let received = [];
|
||||
messages.on('ChannelBanned', cb => {
|
||||
received.push(cb);
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
|
||||
let testBan2 = { ...testBan, externalReason: 'because of reasons' };
|
||||
await controller.banChannel(testBan2);
|
||||
|
||||
let info = await controller.getBannedChannel(testBan2.name);
|
||||
for (let field of Object.keys(testBan2)) {
|
||||
// Consider renaming parameter to avoid this branch
|
||||
if (field === 'name') {
|
||||
assert.strictEqual(info.channelName, testBan2.name);
|
||||
} else {
|
||||
assert.strictEqual(info[field], testBan2[field]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(received, [
|
||||
{
|
||||
channel: testBan.name,
|
||||
externalReason: testBan.externalReason
|
||||
},
|
||||
{
|
||||
channel: testBan2.name,
|
||||
externalReason: testBan2.externalReason
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('unbans a channel', async () => {
|
||||
let received = null;
|
||||
messages.once('ChannelUnbanned', cb => {
|
||||
received = cb;
|
||||
});
|
||||
|
||||
await controller.banChannel(testBan);
|
||||
await controller.unbanChannel(testBan.name, testBan.bannedBy);
|
||||
|
||||
let info = await controller.getBannedChannel(testBan.name);
|
||||
assert.strictEqual(info, null);
|
||||
|
||||
assert.notEqual(received, null);
|
||||
assert.strictEqual(received.channel, testBan.name);
|
||||
});
|
||||
});
|
|
@ -2,7 +2,7 @@
|
|||
"author": "Calvin Montgomery",
|
||||
"name": "CyTube",
|
||||
"description": "Online media synchronizer and chat",
|
||||
"version": "3.83.0",
|
||||
"version": "3.84.0",
|
||||
"repository": {
|
||||
"url": "http://github.com/calzoneman/sync"
|
||||
},
|
||||
|
|
|
@ -94,7 +94,10 @@ function Channel(name) {
|
|||
}, USERCOUNT_THROTTLE);
|
||||
const self = this;
|
||||
db.channels.load(this, function (err) {
|
||||
if (err && err !== "Channel is not registered") {
|
||||
if (err && err.code === 'EBANNED') {
|
||||
self.emit("loadFail", err.message);
|
||||
self.setFlag(Flags.C_ERROR);
|
||||
} else if (err && err !== "Channel is not registered") {
|
||||
self.emit("loadFail", "Failed to load channel data from the database. Please try again later.");
|
||||
self.setFlag(Flags.C_ERROR);
|
||||
} else {
|
||||
|
|
|
@ -404,7 +404,12 @@ KickBanModule.prototype.banAll = async function banAll(
|
|||
);
|
||||
|
||||
if (!await dbIsNameBanned(chan.name, name)) {
|
||||
promises.push(this.banName(actor, name, reason));
|
||||
promises.push(this.banName(actor, name, reason).catch(error => {
|
||||
// TODO: banning should be made idempotent, not throw an error
|
||||
if (!/already banned/.test(error.message)) {
|
||||
throw error;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
|
24
src/cli/banned-channels.js
Normal file
24
src/cli/banned-channels.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import Server from '../server';
|
||||
|
||||
export async function handleBanChannel({ name, externalReason, internalReason }) {
|
||||
await Server.getServer().bannedChannelsController.banChannel({
|
||||
name,
|
||||
externalReason,
|
||||
internalReason,
|
||||
bannedBy: '[console]'
|
||||
});
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
|
||||
export async function handleUnbanChannel({ name }) {
|
||||
await Server.getServer().bannedChannelsController.unbanChannel(name, '[console]');
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
|
||||
export async function handleShowBannedChannel({ name }) {
|
||||
let banInfo = await Server.getServer().bannedChannelsController.getBannedChannel(name);
|
||||
|
||||
return { status: 'success', ban: banInfo };
|
||||
}
|
|
@ -66,7 +66,6 @@ var defaults = {
|
|||
}
|
||||
},
|
||||
"youtube-v3-key": "",
|
||||
"channel-blacklist": [],
|
||||
"channel-path": "r",
|
||||
"channel-save-interval": 5,
|
||||
"max-channels-per-user": 5,
|
||||
|
@ -372,13 +371,6 @@ function preprocessConfig(cfg) {
|
|||
}
|
||||
}
|
||||
|
||||
/* Convert channel blacklist to a hashtable */
|
||||
var tbl = {};
|
||||
cfg["channel-blacklist"].forEach(function (c) {
|
||||
tbl[c.toLowerCase()] = true;
|
||||
});
|
||||
cfg["channel-blacklist"] = tbl;
|
||||
|
||||
/* Check channel path */
|
||||
if(!/^[-\w]+$/.test(cfg["channel-path"])){
|
||||
LOGGER.error("Channel paths may only use the same characters as usernames and channel names.");
|
||||
|
|
67
src/controller/banned-channels.js
Normal file
67
src/controller/banned-channels.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { eventlog } from '../logger';
|
||||
import { SimpleCache } from '../util/simple-cache';
|
||||
const LOGGER = require('@calzoneman/jsli')('BannedChannelsController');
|
||||
|
||||
export class BannedChannelsController {
|
||||
constructor(dbChannels, globalMessageBus) {
|
||||
this.dbChannels = dbChannels;
|
||||
this.globalMessageBus = globalMessageBus;
|
||||
this.cache = new SimpleCache({
|
||||
maxElem: 1000,
|
||||
maxAge: 5 * 60_000
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: add an audit log to the database
|
||||
*/
|
||||
|
||||
async banChannel({ name, externalReason, internalReason, bannedBy }) {
|
||||
LOGGER.info(`Banning channel ${name} (banned by ${bannedBy})`);
|
||||
eventlog.log(`[acp] ${bannedBy} banned channel ${name}`);
|
||||
|
||||
let banInfo = await this.dbChannels.getBannedChannel(name);
|
||||
if (banInfo !== null) {
|
||||
LOGGER.warn(`Channel ${name} is already banned, updating ban reason`);
|
||||
}
|
||||
|
||||
this.cache.delete(name);
|
||||
|
||||
await this.dbChannels.putBannedChannel({
|
||||
name,
|
||||
externalReason,
|
||||
internalReason,
|
||||
bannedBy
|
||||
});
|
||||
|
||||
this.globalMessageBus.emit(
|
||||
'ChannelBanned',
|
||||
{ channel: name, externalReason }
|
||||
);
|
||||
}
|
||||
|
||||
async unbanChannel(name, unbannedBy) {
|
||||
LOGGER.info(`Unbanning channel ${name}`);
|
||||
eventlog.log(`[acp] ${unbannedBy} unbanned channel ${name}`);
|
||||
this.cache.delete(name);
|
||||
|
||||
this.globalMessageBus.emit(
|
||||
'ChannelUnbanned',
|
||||
{ channel: name }
|
||||
);
|
||||
|
||||
await this.dbChannels.removeBannedChannel(name);
|
||||
}
|
||||
|
||||
async getBannedChannel(name) {
|
||||
name = name.toLowerCase();
|
||||
|
||||
let info = this.cache.get(name);
|
||||
if (info === null) {
|
||||
info = await this.dbChannels.getBannedChannel(name);
|
||||
this.cache.put(name, info);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
@ -209,7 +210,9 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
db.query("SELECT * FROM `channels` WHERE owner=?", [owner],
|
||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE c.owner=?", [owner],
|
||||
function (err, res) {
|
||||
if (err) {
|
||||
callback(err, []);
|
||||
|
@ -245,13 +248,28 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
|
||||
db.query("SELECT * FROM `channels` WHERE name=?", chan.name, function (err, res) {
|
||||
db.query("SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c LEFT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE c.name=? " +
|
||||
"UNION " +
|
||||
"SELECT c.*, bc.external_reason as banReason " +
|
||||
"FROM channels c RIGHT OUTER JOIN banned_channels bc " +
|
||||
"ON bc.channel_name = c.name WHERE bc.channel_name=? ",
|
||||
[chan.name, chan.name],
|
||||
function (err, res) {
|
||||
if (err) {
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.length === 0) {
|
||||
if (res.length > 0 && res[0].banReason !== null) {
|
||||
let banError = new Error(`Channel is banned: ${res[0].banReason}`);
|
||||
banError.code = 'EBANNED';
|
||||
callback(banError, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.length === 0 || res[0].id === null) {
|
||||
callback("Channel is not registered", null);
|
||||
return;
|
||||
}
|
||||
|
@ -704,5 +722,63 @@ module.exports = {
|
|||
LOGGER.error(`Failed to update owner_last_seen column for channel ID ${channelId}: ${error}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getBannedChannel: async function getBannedChannel(name) {
|
||||
if (!valid(name)) {
|
||||
throw new Error("Invalid channel name");
|
||||
}
|
||||
|
||||
return await db.getDB().runTransaction(async tx => {
|
||||
let rows = await tx.table('banned_channels')
|
||||
.where({ channel_name: name })
|
||||
.select();
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
channelName: rows[0].channel_name,
|
||||
externalReason: rows[0].external_reason,
|
||||
internalReason: rows[0].internal_reason,
|
||||
bannedBy: rows[0].banned_by,
|
||||
createdAt: rows[0].created_at,
|
||||
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', 'banned_by']
|
||||
));
|
||||
|
||||
return tx.raw(insert.toString() + update.toString());
|
||||
});
|
||||
},
|
||||
|
||||
removeBannedChannel: async function removeBannedChannel(name) {
|
||||
if (!valid(name)) {
|
||||
throw new Error("Invalid channel name");
|
||||
}
|
||||
|
||||
return await db.getDB().runTransaction(async tx => {
|
||||
await tx.table('banned_channels')
|
||||
.where({ channel_name: name })
|
||||
.delete();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -156,4 +156,15 @@ export async function initTables() {
|
|||
t.primary(['type', 'id']);
|
||||
t.index('updated_at');
|
||||
});
|
||||
|
||||
await ensureTable('banned_channels', t => {
|
||||
t.charset('utf8mb4');
|
||||
t.string('channel_name', 30)
|
||||
.notNullable()
|
||||
.unique();
|
||||
t.text('external_reason').notNullable();
|
||||
t.text('internal_reason').notNullable();
|
||||
t.string('banned_by', 20).notNullable();
|
||||
t.timestamps(/* useTimestamps */ true, /* defaultToNow */ true);
|
||||
});
|
||||
}
|
||||
|
|
36
src/main.js
36
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,39 @@ if (!Config.get('debug')) {
|
|||
});
|
||||
}
|
||||
|
||||
async function handleCliCmd(cmd) {
|
||||
try {
|
||||
switch (cmd.command) {
|
||||
case 'ban-channel':
|
||||
return bannedChannels.handleBanChannel(cmd);
|
||||
case 'unban-channel':
|
||||
return bannedChannels.handleUnbanChannel(cmd);
|
||||
case 'show-banned-channel':
|
||||
return bannedChannels.handleShowBannedChannel(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}`);
|
||||
client.write('{"status":"error","error":"internal error"}\n');
|
||||
});
|
||||
} catch (_error) {
|
||||
// eslint no-empty: off
|
||||
}
|
||||
|
||||
if (line === '/reload') {
|
||||
LOGGER.info('Reloading config');
|
||||
try {
|
||||
|
@ -81,9 +111,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',
|
||||
|
|
|
@ -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;
|
||||
|
@ -71,6 +72,7 @@ var Server = function () {
|
|||
const globalMessageBus = this.initModule.getGlobalMessageBus();
|
||||
globalMessageBus.on('UserProfileChanged', this.handleUserProfileChange.bind(this));
|
||||
globalMessageBus.on('ChannelDeleted', this.handleChannelDelete.bind(this));
|
||||
globalMessageBus.on('ChannelBanned', this.handleChannelBanned.bind(this));
|
||||
globalMessageBus.on('ChannelRegistered', this.handleChannelRegister.bind(this));
|
||||
|
||||
// database init ------------------------------------------------------
|
||||
|
@ -108,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);
|
||||
|
@ -134,7 +141,8 @@ var Server = function () {
|
|||
Config.getEmailConfig(),
|
||||
emailController,
|
||||
Config.getCaptchaConfig(),
|
||||
captchaController
|
||||
captchaController,
|
||||
self.bannedChannelsController
|
||||
);
|
||||
|
||||
// http/https/sio server init -----------------------------------------
|
||||
|
@ -549,6 +557,34 @@ Server.prototype.handleChannelDelete = function (event) {
|
|||
}
|
||||
};
|
||||
|
||||
Server.prototype.handleChannelBanned = function (event) {
|
||||
try {
|
||||
const lname = event.channel.toLowerCase();
|
||||
const reason = event.externalReason;
|
||||
|
||||
this.channels.forEach(channel => {
|
||||
if (channel.dead) return;
|
||||
|
||||
if (channel.uniqueName === lname) {
|
||||
channel.clearFlag(Flags.C_REGISTERED);
|
||||
|
||||
const users = Array.prototype.slice.call(channel.users);
|
||||
users.forEach(u => {
|
||||
u.kick(`Channel was banned: ${reason}`);
|
||||
});
|
||||
|
||||
if (!channel.dead && !channel.dying) {
|
||||
channel.emit('empty');
|
||||
}
|
||||
|
||||
LOGGER.info('Processed banned channel %s', lname);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
LOGGER.error('handleChannelBanned failed: %s', error);
|
||||
}
|
||||
};
|
||||
|
||||
Server.prototype.handleChannelRegister = function (event) {
|
||||
try {
|
||||
const lname = event.channel.toLowerCase();
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -76,10 +76,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|||
}
|
||||
|
||||
data.name = data.name.toLowerCase();
|
||||
if (data.name in Config.get("channel-blacklist")) {
|
||||
this.kick("This channel is blacklisted.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.waitFlag(Flags.U_READY, () => {
|
||||
var chan;
|
||||
|
@ -102,10 +98,6 @@ User.prototype.handleJoinChannel = function handleJoinChannel(data) {
|
|||
|
||||
if (!chan.is(Flags.C_READY)) {
|
||||
chan.once("loadFail", reason => {
|
||||
this.socket.emit("errorMsg", {
|
||||
msg: reason,
|
||||
alert: true
|
||||
});
|
||||
this.kick(`Channel could not be loaded: ${reason}`);
|
||||
});
|
||||
}
|
||||
|
|
45
src/util/simple-cache.js
Normal file
45
src/util/simple-cache.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
class SimpleCache {
|
||||
constructor({ maxElem, maxAge }) {
|
||||
this.maxElem = maxElem;
|
||||
this.maxAge = maxAge;
|
||||
this.cache = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
this.cleanup();
|
||||
}, maxAge).unref();
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
this.cache.set(key, { value: value, at: Date.now() });
|
||||
|
||||
if (this.cache.size > this.maxElem) {
|
||||
this.cache.delete(this.cache.keys().next().value);
|
||||
}
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let val = this.cache.get(key);
|
||||
|
||||
if (val != null && Date.now() < val.at + this.maxAge) {
|
||||
return val.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
let now = Date.now();
|
||||
|
||||
for (let [key, value] of this.cache) {
|
||||
if (value.at < now - this.maxAge) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SimpleCache };
|
|
@ -265,6 +265,8 @@ async function handleNewChannel(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
let banInfo = await db.channels.getBannedChannel(name);
|
||||
|
||||
db.channels.listUserChannels(user.name, function (err, channels) {
|
||||
if (err) {
|
||||
sendPug(res, "account-channels", {
|
||||
|
@ -274,6 +276,14 @@ async function handleNewChannel(req, res) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (banInfo !== null) {
|
||||
sendPug(res, "account-channels", {
|
||||
channels: channels,
|
||||
newChannelError: `Cannot register "${name}": this channel is banned.`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (name.match(Config.get("reserved-names.channels"))) {
|
||||
sendPug(res, "account-channels", {
|
||||
channels: channels,
|
||||
|
|
|
@ -4,13 +4,21 @@ import { sendPug } from '../pug';
|
|||
import * as HTTPStatus from '../httpstatus';
|
||||
import { HTTPError } from '../../errors';
|
||||
|
||||
export default function initialize(app, ioConfig, chanPath) {
|
||||
app.get(`/${chanPath}/:channel`, (req, res) => {
|
||||
export default function initialize(app, ioConfig, chanPath, getBannedChannel) {
|
||||
app.get(`/${chanPath}/:channel`, async (req, res) => {
|
||||
if (!req.params.channel || !CyTubeUtil.isValidChannelName(req.params.channel)) {
|
||||
throw new HTTPError(`"${sanitizeText(req.params.channel)}" is not a valid ` +
|
||||
'channel name.', { status: HTTPStatus.NOT_FOUND });
|
||||
}
|
||||
|
||||
let banInfo = await getBannedChannel(req.params.channel);
|
||||
if (banInfo !== null) {
|
||||
sendPug(res, 'banned_channel', {
|
||||
externalReason: banInfo.externalReason
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoints = ioConfig.getSocketEndpoints();
|
||||
if (endpoints.length === 0) {
|
||||
throw new HTTPError('No socket.io endpoints configured');
|
||||
|
|
|
@ -143,7 +143,8 @@ module.exports = {
|
|||
emailConfig,
|
||||
emailController,
|
||||
captchaConfig,
|
||||
captchaController
|
||||
captchaController,
|
||||
bannedChannelsController
|
||||
) {
|
||||
patchExpressToHandleAsync();
|
||||
const chanPath = Config.get('channel-path');
|
||||
|
@ -194,7 +195,12 @@ module.exports = {
|
|||
LOGGER.info('Enabled express-minify for CSS and JS');
|
||||
}
|
||||
|
||||
require('./routes/channel')(app, ioConfig, chanPath);
|
||||
require('./routes/channel')(
|
||||
app,
|
||||
ioConfig,
|
||||
chanPath,
|
||||
async name => bannedChannelsController.getBannedChannel(name)
|
||||
);
|
||||
require('./routes/index')(app, channelIndex, webConfig.getMaxIndexEntries());
|
||||
require('./routes/socketconfig')(app, clusterClient);
|
||||
require('./routes/contact')(app, webConfig);
|
||||
|
|
|
@ -24,7 +24,7 @@ block content
|
|||
tbody
|
||||
for c in channels
|
||||
tr
|
||||
th
|
||||
td
|
||||
form.form-inline.pull-right(action="/account/channels", method="post", onsubmit="return confirm('Are you sure you want to delete " +c.name+ "? This cannot be undone');")
|
||||
input(type="hidden", name="_csrf", value=csrfToken)
|
||||
input(type="hidden", name="action", value="delete_channel")
|
||||
|
@ -32,6 +32,9 @@ block content
|
|||
button.btn.btn-xs.btn-danger(type="submit") Delete
|
||||
span.glyphicon.glyphicon-trash
|
||||
a(href=`/${channelPath}/${c.name}`, style="margin-left: 5px")= c.name
|
||||
if c.banReason != null
|
||||
|
|
||||
span.label.label-danger Banned
|
||||
.col-lg-6.col-md-6
|
||||
h3 Register a new channel
|
||||
if newChannelError
|
||||
|
|
9
templates/banned_channel.pug
Normal file
9
templates/banned_channel.pug
Normal file
|
@ -0,0 +1,9 @@
|
|||
extends layout.pug
|
||||
|
||||
block content
|
||||
.col-md-12
|
||||
.alert.alert-danger
|
||||
h1 Banned Channel
|
||||
strong This channel is banned:
|
||||
p
|
||||
= externalReason
|
52
test/util/simple-cache.js
Normal file
52
test/util/simple-cache.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
const { SimpleCache } = require('../../lib/util/simple-cache');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('SimpleCache', () => {
|
||||
const CACHE_MAX_ELEM = 5;
|
||||
const CACHE_MAX_AGE = 5;
|
||||
let cache;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new SimpleCache({
|
||||
maxElem: CACHE_MAX_ELEM,
|
||||
maxAge: CACHE_MAX_AGE
|
||||
});
|
||||
});
|
||||
|
||||
it('sets, gets, and deletes a value', () => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
|
||||
cache.put('foo', 'bar');
|
||||
assert.strictEqual(cache.get('foo'), 'bar');
|
||||
|
||||
cache.delete('foo');
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
});
|
||||
|
||||
it('does not return an expired value', done => {
|
||||
cache.put('foo', 'bar');
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
done();
|
||||
}, CACHE_MAX_AGE + 1);
|
||||
});
|
||||
|
||||
it('cleans up old values', done => {
|
||||
cache.put('foo', 'bar');
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(cache.get('foo'), null);
|
||||
done();
|
||||
}, CACHE_MAX_AGE * 2);
|
||||
});
|
||||
|
||||
it('removes the oldest entry if max elem is reached', () => {
|
||||
for (let i = 0; i < CACHE_MAX_ELEM + 1; i++) {
|
||||
cache.put(`foo${i}`, 'bar');
|
||||
}
|
||||
|
||||
assert.strictEqual(cache.get('foo0'), null);
|
||||
assert.strictEqual(cache.get('foo1'), 'bar');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue