Переглянути джерело

Added lockdown to backend.

KrisVos130 7 роки тому
батько
коміт
3f43ca07da

+ 3 - 1
README.md

@@ -57,7 +57,9 @@ Once you've installed the required tools:
 	To set up a GitHub OAuth Application, you need to fill in some value's. The homepage is the homepage of frontend. The authorization callback url is the backend url with `/auth/github/authorize/callback` added at the end. For example `http://localhost:8080/auth/github/authorize/callback`.
    	The `apis.recaptcha.secret` value can be obtained by setting up a [ReCaptcha Site](https://www.google.com/recaptcha/admin).  
    	The `apis.github` values can be obtained by setting up a [GitHub OAuth Application](https://github.com/settings/developers).  
-   	`apis.discord` is currently not needed.  
+   	The `apis.discord.token` is the token for the Discord bot.  
+   	The `apis.discord.loggingServer` is the Discord logging server id.  
+   	The `apis.discord.loggingChannel` is the Discord logging channel id.  
    	The `apis.mailgun` values can be obtained by setting up a [Mailgun account](http://www.mailgun.com/).  
    	The `redis.url` url should be left alone for Docker, and changed to `redis://localhost:6379/0` for non-Docker.
    	The `redis.password` should be the Redis password you either put in your `startRedis.cmd` file for Windows, or `docker-compose.yml` for docker.

+ 3 - 2
backend/config/template.json

@@ -17,8 +17,9 @@
 			"redirect_uri": ""
 		},
 		"discord": {
-			"client": "",
-			"secret": ""
+			"token": "",
+			"loggingChannel": "",
+			"loggingServer": ""
 		},
 		"mailgun": {
 			"key": "",

+ 59 - 14
backend/index.js

@@ -18,23 +18,35 @@ const songs = require('./logic/songs');
 const playlists = require('./logic/playlists');
 const cache = require('./logic/cache');
 const notifications = require('./logic/notifications');
+const punishments = require('./logic/punishments');
 const logger = require('./logic/logger');
 const tasks = require('./logic/tasks');
 const config = require('config');
 
 let currentComponent;
+let initializedComponents = [];
+let lockdownB = false;
 
 process.on('uncaughtException', err => {
-	//console.log(`ERROR: ${err.message}`);
-	console.log(`ERROR: ${err.stack}`);
+	if (lockdownB || err.code === 'ECONNREFUSED' || err.code === 'UNCERTAIN_STATE') return;
+	console.log(`UNCAUGHT EXCEPTION: ${err.stack}`);
 });
 
+const getError = (err) => {
+	let error = 'An error occurred.';
+	if (typeof err === "string") error = err;
+	else if (err.message) {
+		if (err.message !== 'Validation failed') error = err.message;
+		else error = err.errors[Object.keys(err.errors)].message;
+	}
+	return error;
+};
+
 client.on('ready', () => {
 	discordClientCBS.forEach((cb) => {
 		cb();
 	});
-	console.log(`DISCORD Logged in as ${client.user.username}!`);
-	//this.logToDiscord("Lost connection to MongoDB. Crashing server.", "Database error", true, [{name: 'Error', value: 'MONGOERR: ERRCNNCT 127.0.0.1 FAILED TO CONNECT', inline: false}], (err) => {console.log(err);});
+	console.log(`Logged in to Discord as ${client.user.username}#${client.user.discriminator}`);
 });
 
 client.login(config.get('apis.discord.token'));
@@ -62,92 +74,126 @@ const logToDiscord = (message, color, type, critical, extraFields, cb = ()=>{})
 		extraFields.forEach((extraField) => {
 			richEmbed.addField(extraField.name, extraField.value, extraField.inline);
 		});
-		/*client.channels.get(config.get('apis.discord.loggingChannel')).sendEmbed(richEmbed).then(() => {
+		client.channels.get(config.get('apis.discord.loggingChannel')).sendEmbed(richEmbed).then(() => {
 			cb();
 		}).then((reason) => {
 			cb(reason);
-		});*/
+		});
 	});
 };
 
+function lockdown() {
+	if (lockdownB) return;
+	lockdownB = true;
+	initializedComponents.forEach((component) => {
+		component._lockdown();
+	});
+	console.log("Backend locked down.");
+}
+
+function errorCb(message, err, component) {
+	err = getError(err);
+	lockdown();
+	logToDiscord(message, "#FF0000", message, true, [{name: "Error:", value: err, inline: false}, {name: "Component:", value: component, inline: true}]);
+}
+
 async.waterfall([
 
 	// setup our Redis cache
 	(next) => {
 		currentComponent = 'Cache';
-		cache.init(config.get('redis').url, config.get('redis').password, () => {
+		cache.init(config.get('redis').url, config.get('redis').password, errorCb, () => {
 			next();
 		});
 	},
 
 	// setup our MongoDB database
 	(next) => {
+		initializedComponents.push(cache);
 		currentComponent = 'DB';
-		db.init(config.get("mongo").url, next);
+		db.init(config.get("mongo").url, errorCb, next);
 	},
 
 	// setup the express server
 	(next) => {
+		initializedComponents.push(db);
 		currentComponent = 'App';
 		app.init(next);
 	},
 
 	// setup the mail
 	(next) => {
+		initializedComponents.push(app);
 		currentComponent = 'Mail';
 		mail.init(next);
 	},
 
 	// setup the socket.io server (all client / server communication is done over this)
 	(next) => {
+		initializedComponents.push(mail);
 		currentComponent = 'IO';
 		io.init(next);
 	},
 
+	// setup the punishment system
+	(next) => {
+		initializedComponents.push(io);
+		currentComponent = 'Punishments';
+		punishments.init(next);
+	},
+
 	// setup the notifications
 	(next) => {
+		initializedComponents.push(punishments);
 		currentComponent = 'Notifications';
-		notifications.init(config.get('redis').url, config.get('redis').password, next);
+		notifications.init(config.get('redis').url, config.get('redis').password, errorCb, next);
 	},
 
 	// setup the stations
 	(next) => {
+		initializedComponents.push(notifications);
 		currentComponent = 'Stations';
 		stations.init(next)
 	},
 
 	// setup the songs
 	(next) => {
+		initializedComponents.push(stations);
 		currentComponent = 'Songs';
 		songs.init(next)
 	},
 
 	// setup the playlists
 	(next) => {
+		initializedComponents.push(songs);
 		currentComponent = 'Playlists';
 		playlists.init(next)
 	},
 
 	// setup the API
 	(next) => {
+		initializedComponents.push(playlists);
 		currentComponent = 'API';
 		api.init(next)
 	},
 
 	// setup the logger
 	(next) => {
+		initializedComponents.push(api);
 		currentComponent = 'Logger';
 		logger.init(next)
 	},
 
 	// setup the tasks system
 	(next) => {
+		initializedComponents.push(logger);
 		currentComponent = 'Tasks';
 		tasks.init(next)
 	},
 
 	// setup the frontend for local setups
 	(next) => {
+		initializedComponents.push(tasks);
 		currentComponent = 'Windows';
 		if (!config.get("isDocker")) {
 			const express = require('express');
@@ -166,17 +212,16 @@ async.waterfall([
 				});
 			});
 		}
+		if (lockdownB) return;
 		next();
 	}
 ], (err) => {
 	if (err && err !== true) {
-		logToDiscord("An error occurred while initializing the backend server.", "#FF0000", "Startup error", true, [{name: "Error:", value: err, inline: false}, {name: "Component:", value: currentComponent, inline: true}])
+		lockdown();
+		logToDiscord("An error occurred while initializing the backend server.", "#FF0000", "Startup error", true, [{name: "Error:", value: err, inline: false}, {name: "Component:", value: currentComponent, inline: true}]);
 		console.error('An error occurred while initializing the backend server');
-		console.error(err);
-
-		process.exit();
 	} else {
-		logToDiscord("The backend server started successfully.", "#00AA00", "Startup", false);
+		logToDiscord("The backend server started successfully.", "#00AA00", "Startup", false, []);
 		console.info('Backend server has been successfully started');
 	}
 });

+ 7 - 0
backend/logic/api.js

@@ -1,3 +1,5 @@
+let lockdown = false;
+
 module.exports = {
 	init: (cb) => {
 		const { app } = require('./app.js');
@@ -22,6 +24,11 @@ module.exports = {
 			})
 		});
 
+		if (lockdown) return this._lockdown();
 		cb();
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 }

+ 13 - 0
backend/logic/app.js

@@ -16,6 +16,8 @@ const cache = require('./cache');
 const db = require('./db');
 
 let utils;
+let initialized = false;
+let lockdown = false;
 
 const lib = {
 
@@ -52,6 +54,7 @@ const lib = {
 		let redirect_uri = config.get('serverDomain') + '/auth/github/authorize/callback';
 
 		app.get('/auth/github/authorize', (req, res) => {
+			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
 			let params = [
 				`client_id=${config.get('apis.github.client')}`,
 				`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
@@ -61,6 +64,7 @@ const lib = {
 		});
 
 		app.get('/auth/github/link', (req, res) => {
+			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
 			let params = [
 				`client_id=${config.get('apis.github.client')}`,
 				`redirect_uri=${config.get('serverDomain')}/auth/github/authorize/callback`,
@@ -75,6 +79,7 @@ const lib = {
 		}
 
 		app.get('/auth/github/authorize/callback', (req, res) => {
+			if (lockdown) return res.json({status: 'failure', message: 'Lockdown'});
 			let code = req.query.code;
 			let access_token;
 			let body;
@@ -231,7 +236,15 @@ const lib = {
 			});
 		});
 
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
+	},
+
+	_lockdown: () => {
+		lib.server.close();
+		lockdown = true;
 	}
 };
 

+ 25 - 13
backend/logic/cache/index.js

@@ -6,12 +6,14 @@ const mongoose = require('mongoose');
 // Lightweight / convenience wrapper around redis module for our needs
 
 const pubs = {}, subs = {};
-let initialized = false;
 let callbacks = [];
+let initialized = false;
+let lockdown = false;
 
 const lib = {
 
 	client: null,
+	errorCb: null,
 	url: '',
 	schemas: {
 		session: require('./schemas/session'),
@@ -29,21 +31,24 @@ const lib = {
 	 * @param {String} password - the password of the redis server
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	init: (url, password, cb) => {
+	init: (url, password, errorCb, cb) => {
+		lib.errorCb = errorCb;
 		lib.url = url;
 		lib.password = password;
 
 		lib.client = redis.createClient({ url: lib.url, password: lib.password });
 		lib.client.on('error', (err) => {
-			console.error(err);
-			process.exit();
+			if (lockdown) return;
+			errorCb('Cache connection error.', err, 'Cache');
 		});
 
-		initialized = true;
 		callbacks.forEach((callback) => {
 			callback();
 		});
 
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
 	},
 
@@ -51,9 +56,11 @@ const lib = {
 	 * Gracefully closes all the Redis client connections
 	 */
 	quit: () => {
-		lib.client.quit();
-		Object.keys(pubs).forEach((channel) => pubs[channel].quit());
-		Object.keys(subs).forEach((channel) => subs[channel].client.quit());
+		if (lib.client.connected) {
+			lib.client.quit();
+			Object.keys(pubs).forEach((channel) => pubs[channel].quit());
+			Object.keys(subs).forEach((channel) => subs[channel].client.quit());
+		}
 	},
 
 	/**
@@ -66,6 +73,7 @@ const lib = {
 	 * @param {Boolean} [stringifyJson=true] - stringify 'value' if it's an Object or Array
 	 */
 	hset: (table, key, value, cb, stringifyJson = true) => {
+		if (lockdown) return cb('Lockdown');
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 		// automatically stringify objects and arrays into JSON
 		if (stringifyJson && ['object', 'array'].includes(typeof value)) value = JSON.stringify(value);
@@ -87,6 +95,7 @@ const lib = {
 	 * @param {Boolean} [parseJson=true] - attempt to parse returned data as JSON
 	 */
 	hget: (table, key, cb, parseJson = true) => {
+		if (lockdown) return cb('Lockdown');
 		if (!key || !table) return typeof cb === 'function' ? cb(null, null) : null;
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 		lib.client.hget(table, key, (err, value) => {
@@ -107,6 +116,7 @@ const lib = {
 	 * @param {Function} cb - gets called when the value has been deleted from Redis or when it returned an error
 	 */
 	hdel: (table, key, cb) => {
+		if (lockdown) return cb('Lockdown');
 		if (!key || !table) return cb(null, null);
 		if (mongoose.Types.ObjectId.isValid(key)) key = key.toString();
 		lib.client.hdel(table, key, (err) => {
@@ -123,6 +133,7 @@ const lib = {
 	 * @param {Boolean} [parseJson=true] - attempts to parse all values as JSON by default
 	 */
 	hgetall: (table, cb, parseJson = true) => {
+		if (lockdown) return cb('Lockdown');
 		if (!table) return cb(null, null);
 		lib.client.hgetall(table, (err, obj) => {
 			if (err) return typeof cb === 'function' ? cb(err) : null;
@@ -160,6 +171,7 @@ const lib = {
 	 * @param {Boolean} [parseJson=true] - parse the message as JSON
 	 */
 	sub: (channel, cb, parseJson = true) => {
+		if (lockdown) return;
 		if (initialized) subToChannel();
 		else {
 			callbacks.push(() => {
@@ -169,10 +181,6 @@ const lib = {
 		function subToChannel() {
 			if (subs[channel] === undefined) {
 				subs[channel] = { client: redis.createClient({ url: lib.url, password: lib.password }), cbs: [] };
-				subs[channel].client.on('error', (err) => {
-					console.error(err);
-					process.exit();
-				});
 				subs[channel].client.on('message', (channel, message) => {
 					if (parseJson) try { message = JSON.parse(message); } catch (e) {}
 					subs[channel].cbs.forEach((cb) => cb(message));
@@ -182,8 +190,12 @@ const lib = {
 
 			subs[channel].cbs.push(cb);
 		}
-	}
+	},
 
+	_lockdown: () => {
+		lib.quit();
+		lockdown = true;
+	}
 };
 
 module.exports = lib;

+ 13 - 3
backend/logic/db/index.js

@@ -19,18 +19,20 @@ const isLength = (string, min, max) => {
 
 mongoose.Promise = bluebird;
 
+let initialized = false;
+let lockdown = false;
+
 let lib = {
 
 	connection: null,
 	schemas: {},
 	models: {},
 
-	init: (url, cb) => {
+	init: (url, errorCb,  cb) => {
 		lib.connection = mongoose.connect(url).connection;
 
 		lib.connection.on('error', err => {
-			console.error('Database error: ' + err);
-			process.exit();
+			errorCb('Database connection error.', err, 'DB');
 		});
 
 		lib.connection.once('open', _ => {
@@ -186,6 +188,9 @@ let lib = {
 				return (!description || (isLength(description, 0, 400) && regex.ascii.test(description)));
 			}, 'Invalid description.');
 
+			initialized = true;
+
+			if (lockdown) return this._lockdown();
 			cb();
 		});
 	},
@@ -193,6 +198,11 @@ let lib = {
 	passwordValid: (password) => {
 		if (!isLength(password, 6, 200)) return false;
 		return regex.password.test(password);
+	},
+
+	_lockdown: () => {
+		lib.connection.close();
+		lockdown = true;
 	}
 };
 

+ 19 - 1
backend/logic/io.js

@@ -11,6 +11,9 @@ const db = require('./db');
 const logger = require('./logger');
 const punishments = require('./punishments');
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 
 	io: null,
@@ -20,6 +23,7 @@ module.exports = {
 		this.io = require('socket.io')(app.server);
 
 		this.io.use((socket, next) => {
+			if (lockdown) return;
 			let cookies = socket.request.headers.cookie;
 			let SID = utils.cookies.parseCookies(cookies).SID;
 
@@ -66,6 +70,7 @@ module.exports = {
 		});
 
 		this.io.on('connection', socket => {
+			if (lockdown) return socket.disconnect(true);
 			let sessionInfo = '';
 			if (socket.session.sessionId) sessionInfo = ` UserID: ${socket.session.userId}.`;
 			if (socket.banned) {
@@ -94,10 +99,11 @@ module.exports = {
 
 						// listen for this action to be called
 						socket.on(name, function () {
-
 							let args = Array.prototype.slice.call(arguments, 0, -1);
 							let cb = arguments[arguments.length - 1];
 
+							if (lockdown) return cb({status: 'failure', message: 'Lockdown'});
+
 							// load the session from the cache
 							cache.hget('sessions', socket.session.sessionId, (err, session) => {
 								if (err && err !== true) {
@@ -144,7 +150,19 @@ module.exports = {
 			}
 		});
 
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
+	},
+
+	_lockdown: () => {
+		this.io.close();
+		let connected = this.io.of('/').connected;
+		for (let key in connected) {
+			connected[key].disconnect('Lockdown');
+		}
+		lockdown = true;
 	}
 
 };

+ 20 - 2
backend/logic/logger.js

@@ -89,6 +89,9 @@ let getTimeFormatted = () => {
 	return `${time.year}-${twoDigits(time.month)}-${twoDigits(time.day)} ${twoDigits(time.hour)}:${twoDigits(time.minute)}:${twoDigits(time.second)}`;
 }
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 	init: function(cb) {
 		utils = require('./utils');
@@ -104,11 +107,14 @@ module.exports = {
 		fs.appendFile(dir + '/error.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
 		fs.appendFile(dir + '/info.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
 		fs.appendFile(dir + '/debugStation.log', `${time} BACKEND_RESTARTED\n`, ()=>{});
-		cb("MAJOR FAILURE!");
 
-		//cb();
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
+		cb();
 	},
 	success: (type, message, display = true) => {
+		if (lockdown) return;
 		success++;
 		successThisMinute++;
 		successThisHour++;
@@ -118,6 +124,7 @@ module.exports = {
 		if (display) console.info('\x1b[32m', time, 'SUCCESS', '-', type, '-', message, '\x1b[0m');
 	},
 	error: (type, message, display = true) => {
+		if (lockdown) return;
 		error++;
 		errorThisMinute++;
 		errorThisHour++;
@@ -127,6 +134,7 @@ module.exports = {
 		if (display) console.warn('\x1b[31m', time, 'ERROR', '-', type, '-', message, '\x1b[0m');
 	},
 	info: (type, message, display = true) => {
+		if (lockdown) return;
 		info++;
 		infoThisMinute++;
 		infoThisHour++;
@@ -136,28 +144,34 @@ module.exports = {
 		if (display) console.info('\x1b[36m', time, 'INFO', '-', type, '-', message, '\x1b[0m');
 	},
 	stationIssue: (string, display = false) => {
+		if (lockdown) return;
 		let time = getTimeFormatted();
 		fs.appendFile(dir + '/debugStation.log', `${time} - ${string}\n`, ()=>{});
 		if (display) console.info('\x1b[35m', time, '-', string, '\x1b[0m');
 	},
 	calculatePerSecond: function(number) {
+		if (lockdown) return;
 		let secondsRunning = Math.floor((Date.now() - started) / 1000);
 		let perSecond = number / secondsRunning;
 		return perSecond;
 	},
 	calculatePerMinute: function(number) {
+		if (lockdown) return;
 		let perMinute = this.calculatePerSecond(number) * 60;
 		return perMinute;
 	},
 	calculatePerHour: function(number) {
+		if (lockdown) return;
 		let perHour = this.calculatePerMinute(number) * 60;
 		return perHour;
 	},
 	calculatePerDay: function(number) {
+		if (lockdown) return;
 		let perDay = this.calculatePerHour(number) * 24;
 		return perDay;
 	},
 	calculate: function() {
+		if (lockdown) return;
 		let _this = module.exports;
 		utils.emitToRoom('admin.statistics', 'event:admin.statistics.logs', {
 			second: {
@@ -182,5 +196,9 @@ module.exports = {
 			}
 		});
 		setTimeout(_this.calculate, 1000 * 30);
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 };

+ 11 - 0
backend/logic/mail/index.js

@@ -10,6 +10,9 @@ if (enabled) {
 	});
 }
 
+let initialized = false;
+let lockdown = false;
+
 let lib = {
 
 	schemas: {},
@@ -21,13 +24,21 @@ let lib = {
 			passwordRequest: require('./schemas/passwordRequest')
 		};
 
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
 	},
 
 	sendMail: (data, cb) => {
+		if (lockdown) return cb('Lockdown');
 		if (!cb) cb = ()=>{};
 		if (enabled) mailgun.messages().send(data, cb);
 		else cb();
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 };
 

+ 32 - 13
backend/logic/notifications.js

@@ -4,13 +4,18 @@ const crypto = require('crypto');
 const redis = require('redis');
 const logger = require('./logger');
 
-let pub = null;
-let sub = null;
-
 const subscriptions = [];
 
+let initialized = false;
+let lockdown = false;
+let errorCb;
+
 const lib = {
 
+	pub: null,
+	sub: null,
+	errorCb: null,
+
 	/**
 	 * Initializes the notifications module
 	 *
@@ -18,21 +23,25 @@ const lib = {
 	 * @param {String} password - the password of the redis server
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
-	init: (url, password, cb) => {
-		pub = redis.createClient({ url, password });
-		sub = redis.createClient({ url, password });
-		sub.on('error', (err) => {
-			console.error(err);
-			process.exit();
+	init: (url, password, errorCb, cb) => {
+		lib.errorCb = errorCb;
+		lib.pub = redis.createClient({ url, password });
+		lib.sub = redis.createClient({ url, password });
+		lib.sub.on('error', (err) => {
+			errorCb('Cache connection error.', err, 'Notifications');
 		});
-		sub.on('pmessage', (pattern, channel, expiredKey) => {
+		lib.sub.on('pmessage', (pattern, channel, expiredKey) => {
 			logger.stationIssue(`PMESSAGE - Pattern: ${pattern}; Channel: ${channel}; ExpiredKey: ${expiredKey}`);
 			subscriptions.forEach((sub) => {
 				if (sub.name !== expiredKey) return;
 				sub.cb();
 			});
 		});
-		sub.psubscribe('__keyevent@0__:expired');
+		lib.sub.psubscribe('__keyevent@0__:expired');
+
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
 	},
 
@@ -46,10 +55,11 @@ const lib = {
 	 * @param {Function} cb - gets called when the notification has been scheduled
 	 */
 	schedule: (name, time, cb, station) => {
+		if (lockdown) return;
 		if (!cb) cb = ()=>{};
 		time = Math.round(time);
 		logger.stationIssue(`SCHEDULE - Time: ${time}; Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}; StationId: ${station._id}; StationName: ${station.name}`);
-		pub.set(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), '', 'PX', time, 'NX', cb);
+		lib.pub.set(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), '', 'PX', time, 'NX', cb);
 	},
 
 	/**
@@ -61,6 +71,7 @@ const lib = {
 	 * @return {Object} - the subscription object
 	 */
 	subscribe: (name, cb, unique = false, station) => {
+		if (lockdown) return;
 		logger.stationIssue(`SUBSCRIBE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}, StationId: ${station._id}; StationName: ${station.name}; Unique: ${unique}; SubscriptionExists: ${!!subscriptions.find((subscription) => subscription.originalName == name)};`);
 		if (unique && !!subscriptions.find((subscription) => subscription.originalName == name)) return;
 		let subscription = { originalName: name, name: crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'), cb };
@@ -74,14 +85,22 @@ const lib = {
 	 * @param {Object} subscription - the subscription object returned by {@link subscribe}
 	 */
 	remove: (subscription) => {
+		if (lockdown) return;
 		let index = subscriptions.indexOf(subscription);
 		if (index) subscriptions.splice(index, 1);
 	},
 
 	unschedule: (name) => {
+		if (lockdown) return;
 		logger.stationIssue(`UNSCHEDULE - Name: ${name}; Key: ${crypto.createHash('md5').update(`_notification:${name}_`).digest('hex')}`);
-		pub.del(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'));
+		lib.pub.del(crypto.createHash('md5').update(`_notification:${name}_`).digest('hex'));
 	},
+
+	_lockdown: () => {
+		lib.pub.quit();
+		lib.sub.quit();
+		lockdown = true;
+	}
 };
 
 module.exports = lib;

+ 14 - 2
backend/logic/playlists.js

@@ -4,6 +4,9 @@ const cache = require('./cache');
 const db = require('./db');
 const async = require('async');
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 
 	/**
@@ -41,10 +44,12 @@ module.exports = {
 				}, next);
 			}
 		], (err) => {
+			if (lockdown) return this._lockdown();
 			if (err) {
-				console.log(`FAILED TO INITIALIZE PLAYLISTS. ABORTING. "${err.message}"`);
-				process.exit();
+				err = utils.getError(err);
+				cb(err);
 			} else {
+				initialized = true;
 				cb();
 			}
 		});
@@ -57,6 +62,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getPlaylist: (playlistId, cb) => {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
 				cache.hgetall('playlists', next);
@@ -104,6 +110,7 @@ module.exports = {
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
 	updatePlaylist: (playlistId, cb) => {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -131,6 +138,7 @@ module.exports = {
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
 	deletePlaylist: (playlistId, cb) => {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -146,5 +154,9 @@ module.exports = {
 
 			cb(null);
 		});
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 };

+ 19 - 3
backend/logic/punishments.js

@@ -7,6 +7,9 @@ const utils = require('./utils');
 const async = require('async');
 const mongoose = require('mongoose');
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 
 	/**
@@ -43,10 +46,14 @@ module.exports = {
 				}, next);
 			}
 		], (err) => {
+			if (lockdown) return this._lockdown();
 			if (err) {
-				console.log(`FAILED TO INITIALIZE PUNISHMENTS. ABORTING. "${err.message}"`);
-				process.exit();
-			} else cb();
+				err = utils.getError(err);
+				cb(err);
+			} else {
+				initialized = true;
+				cb();
+			}
 		});
 	},
 
@@ -56,6 +63,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getPunishments: function(cb) {
+		if (lockdown) return cb('Lockdown');
 		let punishmentsToRemove = [];
 		async.waterfall([
 			(next) => {
@@ -103,6 +111,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getPunishment: function(id, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -135,6 +144,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getPunishmentsFromUserId: function(userId, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
 				module.exports.getPunishments(next);
@@ -153,6 +163,7 @@ module.exports = {
 	},
 
 	addPunishment: function(type, value, reason, expiresAt, punishedBy, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
 				const punishment = new db.models.punishment({
@@ -185,6 +196,7 @@ module.exports = {
 	},
 
 	removePunishmentFromCache: function(punishmentId, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
 				const punishment = new db.models.punishment({
@@ -214,5 +226,9 @@ module.exports = {
 		], (err) => {
 			cb(err);
 		});
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 };

+ 18 - 3
backend/logic/songs.js

@@ -7,6 +7,9 @@ const utils = require('./utils');
 const async = require('async');
 const mongoose = require('mongoose');
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 
 	/**
@@ -42,10 +45,14 @@ module.exports = {
 				}, next);
 			}
 		], (err) => {
+			if (lockdown) return this._lockdown();
 			if (err) {
-				console.log(`FAILED TO INITIALIZE SONGS. ABORTING. "${err.message}"`);
-				process.exit();
-			} else cb();
+				err = utils.getError(err);
+				cb(err);
+			} else {
+				initialized = true;
+				cb();
+			}
 		});
 	},
 
@@ -56,6 +63,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getSong: function(id, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -88,6 +96,7 @@ module.exports = {
 	 * @param {Function} cb - gets called once we're done initializing
 	 */
 	getSongFromId: function(songId, cb) {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 			(next) => {
 				db.models.song.findOne({ _id: songId }, next);
@@ -105,6 +114,7 @@ module.exports = {
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
 	updateSong: (songId, cb) => {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -134,6 +144,7 @@ module.exports = {
 	 * @param {Function} cb - gets called when an error occurred or when the operation was successful
 	 */
 	deleteSong: (songId, cb) => {
+		if (lockdown) return cb('Lockdown');
 		async.waterfall([
 
 			(next) => {
@@ -149,5 +160,9 @@ module.exports = {
 
 			cb(null);
 		});
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 };

+ 26 - 5
backend/logic/stations.js

@@ -9,16 +9,22 @@ const songs = require('./songs');
 const notifications = require('./notifications');
 const async = require('async');
 
+let initialized = false;
+let lockdown = false;
+
 //TEMP
 cache.sub('station.pause', (stationId) => {
+	if (lockdown) return;
 	notifications.remove(`stations.nextSong?id=${stationId}`);
 });
 
 cache.sub('station.resume', (stationId) => {
+	if (lockdown) return;
 	module.exports.initializeStation(stationId)
 });
 
 cache.sub('station.queueUpdate', (stationId) => {
+	if (lockdown) return;
 	module.exports.getStation(stationId, (err, station) => {
 		if (!station.currentSong && station.queue.length > 0) {
 			module.exports.initializeStation(stationId);
@@ -27,6 +33,7 @@ cache.sub('station.queueUpdate', (stationId) => {
 });
 
 cache.sub('station.newOfficialPlaylist', (stationId) => {
+	if (lockdown) return;
 	cache.hget("officialPlaylists", stationId, (err, playlistObj) => {
 		if (!err && playlistObj) {
 			utils.emitToRoom(`station.${stationId}`, "event:newOfficialPlaylist", playlistObj.songs);
@@ -75,14 +82,19 @@ module.exports = {
 				}, next);
 			}
 		], (err) => {
+			if (lockdown) return this._lockdown();
 			if (err) {
-				console.log(`FAILED TO INITIALIZE STATIONS. ABORTING. "${err.message}"`);
-				process.exit();
-			} else cb();
+				err = utils.getError(err);
+				cb(err);
+			} else {
+				initialized = true;
+				cb();
+			}
 		});
 	},
 
 	initializeStation: function(stationId, cb) {
+		if (lockdown) return;
 		if (typeof cb !== 'function') cb = ()=>{};
 		let _this = this;
 		async.waterfall([
@@ -123,10 +135,10 @@ module.exports = {
 	},
 
 	calculateSongForStation: function(station, cb) {
+		if (lockdown) return;
 		let _this = this;
 		let songList = [];
 		async.waterfall([
-
 			(next) => {
 				let genresDone = [];
 				station.genres.forEach((genre) => {
@@ -181,6 +193,7 @@ module.exports = {
 
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
 	getStation: function(stationId, cb) {
+		if (lockdown) return;
 		let _this = this;
 		async.waterfall([
 			(next) => {
@@ -211,6 +224,7 @@ module.exports = {
 
 	// Attempts to get the station from Redis. If it's not in Redis, get it from Mongo and add it to Redis.
 	getStationByName: function(stationName, cb) {
+		if (lockdown) return;
 		let _this = this;
 		async.waterfall([
 
@@ -236,6 +250,7 @@ module.exports = {
 	},
 
 	updateStation: function(stationId, cb) {
+		if (lockdown) return;
 		let _this = this;
 		async.waterfall([
 
@@ -258,8 +273,8 @@ module.exports = {
 	},
 
 	calculateOfficialPlaylistList: (stationId, songList, cb) => {
+		if (lockdown) return;
 		let lessInfoPlaylist = [];
-
 		async.each(songList, (song, next) => {
 			songs.getSongFromId(song, (err, song) => {
 				if (!err && song) {
@@ -282,9 +297,11 @@ module.exports = {
 	},
 
 	skipStation: function(stationId) {
+		if (lockdown) return;
 		logger.info("STATION_SKIP", `Skipping station ${stationId}.`, false);
 		let _this = this;
 		return (cb) => {
+			if (lockdown) return;
 			if (typeof cb !== 'function') cb = ()=>{};
 
 			async.waterfall([
@@ -466,6 +483,10 @@ module.exports = {
 		skipDuration: 0,
 		likes: -1,
 		dislikes: -1
+	},
+
+	_lockdown: () => {
+		lockdown = true;
 	}
 
 };

+ 16 - 1
backend/logic/tasks.js

@@ -90,6 +90,9 @@ let sessionClearingTask = (callback) => {
 	});
 };
 
+let initialized = false;
+let lockdown = false;
+
 module.exports = {
 	init: function(cb) {
 		utils = require('./utils');
@@ -97,9 +100,13 @@ module.exports = {
 		this.createTask("stationSkipTask", checkStationSkipTask, 1000 * 60 * 30);
 		this.createTask("sessionClearTask", sessionClearingTask, 1000 * 60 * 60 * 6);
 
+		initialized = true;
+
+		if (lockdown) return this._lockdown();
 		cb();
 	},
 	createTask: function(name, fn, timeout, paused = false) {
+		if (lockdown) return;
 		tasks[name] = {
 			name,
 			fn,
@@ -110,12 +117,13 @@ module.exports = {
 		if (!paused) this.handleTask(tasks[name]);
 	},
 	pauseTask: (name) => {
-		tasks[name].timer.pause();
+		if (tasks[name].timer) tasks[name].timer.pause();
 	},
 	resumeTask: (name) => {
 		tasks[name].timer.resume();
 	},
 	handleTask: function(task) {
+		if (lockdown) return;
 		if (task.timer) task.timer.pause();
 
 		task.fn(() => {
@@ -124,5 +132,12 @@ module.exports = {
 				this.handleTask(task);
 			}, task.timeout, false);
 		});
+	},
+	_lockdown: function() {
+		for (let key in tasks) {
+			this.pauseTask(key);
+		}
+		tasks = {};
+		lockdown = true;
 	}
 };