diff --git a/config.template.yaml b/config.template.yaml index 269428ad4..e88873e1c 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -237,3 +237,9 @@ setuid: # concurrent updates), then run `node lib/channel-storage/migrate.js`. channel-storage: type: 'file' + +# Allows for external services to access the system commandline +# Useful for setups where stdin isn't available such as when using PM2 +service-socket: + enabled: false + socket: 'service.sock' diff --git a/index.js b/index.js index 729d6cb45..a364c7a1d 100644 --- a/index.js +++ b/index.js @@ -45,6 +45,7 @@ process.stdin.on("data", function (data) { } }); +var validIP = require('net').isIP; function handleLine(line) { if (line === "/reload") { Logger.syslog.log("Reloading config"); @@ -75,5 +76,46 @@ function handleLine(line) { } } else if (line.indexOf("/reload-partitions") === 0) { sv.reloadPartitionMap(); + } else if (line.indexOf("/globalban") === 0) { + var args = line.split(/\s+/); args.shift(); + if (args.length >= 2 && validIP(args[0]) !== 0) { + var ip = args.shift(); + var comment = args.join(' '); + require("./lib/database").globalBanIP(ip, comment, function (err, res) { + if (!err) { + Logger.eventlog.log("[acp] " + "SYSTEM" + " global banned " + ip); + } + }) + } + } else if (line.indexOf("/unglobalban") === 0) { + var args = line.split(/\s+/); args.shift(); + if (args.length >= 1 && validIP(args[0]) !== 0) { + var ip = args.shift(); + require("./lib/database").globalUnbanIP(ip, function (err, res) { + if (!err) { + Logger.eventlog.log("[acp] " + "SYSTEM" + " un-global banned " + ip); + } + }) + } + } else if (line.indexOf("/unloadchan") === 0) { + var args = line.split(/\s+/); args.shift(); + if(args.length){ + var name = args.shift(); + var chan = sv.getChannel(name); + var users = Array.prototype.slice.call(chan.users); + chan.emit("empty"); + users.forEach(function (u) { + u.kick("Channel shutting down"); + }); + Logger.eventlog.log("[acp] " + "SYSTEM" + " forced unload of " + name); + } } } + +// Go Go Gadget Service Socket +if (Config.get("service-socket.enabled")) { + Logger.syslog.log("Opening service socket"); + var ServiceSocket = require('./lib/servsock'); + var server = new ServiceSocket; + server.init(handleLine, Config.get("service-socket.socket")); +} diff --git a/servcmd.sh.js b/servcmd.sh.js new file mode 100755 index 000000000..bb493368f --- /dev/null +++ b/servcmd.sh.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +/* +** CyTube Service Socket Commandline +*/ + +const readline = require('readline'); +const spawn = require('child_process').spawn; +const util = require('util'); +const net = require('net'); +const fs = require('fs'); + +const COMPLETIONS = [ + "/delete_old_tables", + "/gc", + "/globalban", + "/reload", + "/reload-partitions", + "/switch", + "/unglobalban", + "/unloadchan" +]; + +var 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 SOCKETFILE = Config.get("service-socket.socket"); + +// Wipe the TTY +process.stdout.write('\x1Bc'); + +var commandline, eventlog, syslog; +var client = net.createConnection(SOCKETFILE).on('connect', () => { + commandline = readline.createInterface({ + input: process.stdin, + output: process.stdout, + completer: tabcomplete + }); + commandline.setPrompt("> ", 2); + commandline.on("line", function(line) { + if(line === 'exit'){ return cleanup(); } + if(line === 'quit'){ return cleanup(); } + if(line.match(/^\/globalban/) && line.split(/\s+/).length === 2){ + console.log('You must provide a reason') + return commandline.prompt(); + } + client.write(line); + commandline.prompt(); + }); + commandline.on('close', function() { + return cleanup(); + }); + commandline.on("SIGINT", function() { + commandline.clearLine(); + commandline.question("Terminate connection? ", function(answer) { + return answer.match(/^y(es)?$/i) ? cleanup() : commandline.output.write("> "); + }); + }); + commandline.prompt(); + + console.log = function() { cmdouthndlr("log", arguments); } + console.warn = function() { cmdouthndlr("warn", arguments); } + console.error = function() { cmdouthndlr("error", arguments); } + // console.info is reserved in this script for the exit message + // this prevents an extraneous final prompt from readline on terminate + + eventlog = spawn('tail', ['-f', 'events.log']); + eventlog.stdout.on('data', function (data) { + console.log(data.toString().replace(/^(.+)$/mg, 'events: $1')); + }); + + syslog = spawn('tail', ['-f', 'sys.log']); + syslog.stdout.on('data', function (data) { + console.log(data.toString().replace(/^(.+)$/mg, 'sys: $1')); + }); + + }).on('data', (msg) => { + msg = msg.toString(); + + if(msg === '__disconnect'){ + console.log('Server shutting down.'); + return cleanup(); + } + + // Generic message handler + console.log('server: ', data) + + }).on('error', (data) => { + console.error('Unable to connect to Service Socket.', data); + process.exit(1); + }); + +function cmdouthndlr(type, args) { + var t = Math.ceil((commandline.line.length + 3) / process.stdout.columns); + var text = util.format.apply(console, args); + commandline.output.write("\n\x1B[" + t + "A\x1B[0J"); + commandline.output.write(text + "\n"); + commandline.output.write(Array(t).join("\n\x1B[E")); + commandline._refreshLine(); +} + +function cleanup(){ + console.info('\n',"Terminating.",'\n'); + eventlog.kill('SIGTERM'); + syslog.kill('SIGTERM'); + client.end(); + process.exit(0); +} + +function tabcomplete(line) { + return [COMPLETIONS.filter((cv)=>{ return cv.indexOf(line) == 0; }), line]; +} diff --git a/src/config.js b/src/config.js index 6b24f2ae7..e3d0ca825 100644 --- a/src/config.js +++ b/src/config.js @@ -116,6 +116,10 @@ var defaults = { }, "channel-storage": { type: "file" + }, + "service-socket": { + enabled: false, + socket: "service.sock" } }; diff --git a/src/servsock.js b/src/servsock.js new file mode 100644 index 000000000..b368112d4 --- /dev/null +++ b/src/servsock.js @@ -0,0 +1,53 @@ +var fs = require('fs'); +var net = require('net'); + +export default class ServiceSocket { + + constructor() { + this.connections = {}; + } + + init(handler, socket){ + this.handler = handler; + this.socket = socket; + + fs.stat(this.socket, (err, stats) => { + if (err) { + return this.openServiceSocket(); + } + fs.unlink(this.socket, (err) => { + if(err){ + console.error(err); process.exit(0); + } + return this.openServiceSocket(); + }); + }); + } + + openServiceSocket(){ + this.server = net.createServer((stream) => { + let id = Date.now(); + this.connections[id] = stream; + stream.on('end', () => { + delete this.connections[id]; + }); + stream.on('data', (msg) => { + this.handler(msg.toString()); + }); + }).listen(this.socket); + process.on('exit', this.closeServiceSocket.bind(this)); + } + + closeServiceSocket() { + if(Object.keys(this.connections).length){ + let clients = Object.keys(this.connections); + while(clients.length){ + let client = clients.pop(); + this.connections[client].write('__disconnect'); + this.connections[client].end(); + } + } + this.server.close(); + } + +}