Improved exception handling, started work on improving error messages for IP bans

This commit is contained in:
rainbow napkin 2025-05-29 09:48:48 -04:00
parent 7d3c31f0aa
commit 853f67fe15
15 changed files with 118 additions and 77 deletions

View file

@ -122,7 +122,7 @@ module.exports = class{
const userDB = await userModel.findOne({user: socket.request.session.user.user});
if(userDB == null){
throw new Error("User not found!");
throw loggerUtils.exceptionSmith("User not found!", "unauthorized");
}
//Set socket user and channel values
@ -140,7 +140,7 @@ module.exports = class{
//Check if channel exists
if(chanDB == null){
throw new Error("Channel not found!");
throw loggerUtils.exceptionSmith("Channel not found", "validation");
}
//Check if current channel is active

View file

@ -312,7 +312,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue");
}
//For each item
@ -409,7 +409,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue");
}
//Keep a copy of the archive that hasn't been changed
@ -470,7 +470,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while queue item!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue");
}
//Filter media out by UUID
@ -611,7 +611,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while saving item to queue!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while saving item to queue!`, "queue");
}
//Add media to the persistant schedule
@ -766,7 +766,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while ending queue item!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while ending queue item!`, "queue");
}
//If we haven't changed 'nowPlaying' in the play list
@ -909,7 +909,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while rehydrating queue!`, "queue");
}
//Create an empty array to hold our schedule
@ -957,7 +957,7 @@ module.exports = class{
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw new Error(`Unable to find channel document ${this.channel.name} while rehydrating queue!`);
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while rehydrating queue!`, "queue");
}
const now = new Date().getTime();

View file

@ -31,7 +31,7 @@ module.exports.post = async function(req, res){
const channel = await channelModel.findOne({name: data.chanName});
if(channel == null){
throw new Error("Chanenl does not exist!");
throw loggerUtils.exceptionSmith("Chanenl does not exist!", "validation");
}
await channel.nuke(data.confirm);

View file

@ -33,7 +33,7 @@ module.exports.get = async function(req, res){
if(channel == null){
throw new Error("Channel not found.");
throw loggerUtils.exceptionSmith("Channel not found.", "validation");
}
res.status(200);
@ -64,7 +64,7 @@ module.exports.post = async function(req, res){
var permError = null;
if(chanDB == null){
throw new Error("Channel not found.");
throw loggerUtils.exceptionSmith("Channel not found.", "validation");
}
//For each permission submitted

View file

@ -31,7 +31,7 @@ module.exports.get = async function(req, res){
const channel = await channelModel.findOne({name: data.chanName});
if(channel == null){
throw new Error("Channel not found.");
throw loggerUtils.exceptionSmith("Channel not found.", "validation");
}
res.status(200);
@ -56,7 +56,7 @@ module.exports.post = async function(req, res){
const settingsMap = new Map(Object.entries(data.settingsMap));
if(channel == null){
throw new Error("Channel not found.");
throw loggerUtils.exceptionSmith("Channel not found.", "validation");
}
res.status(200);

View file

@ -36,7 +36,7 @@ module.exports.get = async function(req, res){
delete chanDB.permissions._doc._id;
if(chanDB == null){
throw new Error("Channel not found.");
throw loggerUtils.exceptionSmith("Channel not found.", "queue");
}
return res.render('channelSettings', {instance: config.instanceName, user: req.session.user, channel: chanDB, reqRank, rankEnum: permissionModel.rankEnum, csrfToken: csrfUtils.generateToken(req)});

View file

@ -115,7 +115,7 @@ const channelSchema = new mongoose.Schema({
channelSchema.pre('save', async function (next){
if(this.isModified("name")){
if(this.name.match(/^[a-z0-9_\-.]+$/i) == null){
throw new Error("Username must only contain alpha-numerics and the following symbols: '-_.'");
throw loggerUtils.exceptionSmith("Username must only contain alpha-numerics and the following symbols: '-_.'", "validation");
}
}
@ -221,7 +221,7 @@ channelSchema.statics.register = async function(channelObj, ownerObj){
const chanDB = await this.findOne({ name });
if(chanDB){
throw new Error("Channel name already taken!");
throw loggerUtils.exceptionSmith("Channel name already taken!", "validation");
}else{
const id = await statModel.incrementChannelCount();
const rankList = [{
@ -334,7 +334,7 @@ channelSchema.statics.processExpiredBans = async function(){
channelSchema.methods.updateSettings = async function(settingsMap){
settingsMap.forEach((value, key) => {
if(this.settings[key] == null){
throw new Error("Invalid channel setting.");
throw loggerUtils.exceptionSmith("Invalid channel setting.", "validation");
}
this.settings[key] = value;
@ -661,13 +661,13 @@ channelSchema.methods.getChanBans = async function(){
channelSchema.methods.banByUserDoc = async function(userDB, expirationDays, banAlts){
//Throw a shitfit if the user doesn't exist
if(userDB == null){
throw new Error("Cannot ban non-existant user!");
throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation");
}
const foundBan = await this.checkBanByUserDoc(userDB);
if(foundBan != null){
throw new Error("User already banned!");
throw loggerUtils.exceptionSmith("User already banned!", "validation");
}
//Create a new ban document based on input
@ -703,13 +703,13 @@ channelSchema.methods.ban = async function(user, expirationDays, banAlts){
channelSchema.methods.unbanByUserDoc = async function(userDB){
//Throw a shitfit if the user doesn't exist
if(userDB == null){
throw new Error("Cannot ban non-existant user!");
throw loggerUtils.exceptionSmith("Cannot ban non-existant user!", "validation");
}
const foundBan = await this.checkBanByUserDoc(userDB);
if(foundBan == null){
throw new Error("User already unbanned!");
throw loggerUtils.exceptionSmith("User already unbanned!", "validation");
}
//You know I can't help but feel like an asshole for looking for the index of something I just pulled out of an array using forEach...
@ -727,16 +727,16 @@ channelSchema.methods.unban = async function(user){
channelSchema.methods.nuke = async function(confirm){
if(confirm == "" || confirm == null){
throw new Error("Empty Confirmation String!");
throw loggerUtils.exceptionSmith("Empty Confirmation String!", "validation");
}else if(confirm != this.name){
throw new Error("Bad Confirmation String!");
throw loggerUtils.exceptionSmith("Bad Confirmation String!", "validation");
}
//Annoyingly there isnt a good way to do this from 'this'
var oldChan = await this.deleteOne();
if(oldChan.deletedCount == 0){
throw new Error("Server Error: Unable to delete channel! Please report this error to your server administrator, and with timestamp.");
throw loggerUtils.exceptionSmith("Server Error: Unable to delete channel! Please report this error to your server administrator, and with timestamp.", "internal");
}
}

View file

@ -201,7 +201,7 @@ permissionSchema.methods.permCheckByUserDoc = function(userDB, perm){
return (userRank >= requiredRank);
}else{
//if not scream and shout
throw new Error(`Permission check '${perm}' not found!`);
throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "Validation");
}
}
@ -223,7 +223,7 @@ permissionSchema.methods.overrideCheckByUserDoc = function(userDB, perm){
return (userRank >= requiredRank);
}else{
//if not scream and shout
throw new Error(`Permission check '${perm}' not found!`);
throw loggerUtils.exceptionSmith(`Permission check '${perm}' not found!`, "validation");
}
}

View file

@ -27,7 +27,8 @@ const crypto = require("node:crypto");
const {mongoose} = require('mongoose');
//Local Imports
const hashUtil = require('../../utils/hashUtils');
const hashUtil = require('../../utils/hashUtils.js');
const loggerUtils = require('../../utils/loggerUtils.js')
const daysToExpire = 7;
@ -85,7 +86,7 @@ passwordResetSchema.statics.processExpiredRequests = async function(){
passwordResetSchema.methods.consume = async function(pass, confirmPass){
//Check confirmation pass
if(pass != confirmPass){
throw new Error("Confirmation password does not match!");
throw loggerUtils.exceptionSmith("Confirmation password does not match!", "validation");
}
//Populate the user reference

View file

@ -18,8 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const {mongoose} = require('mongoose');
//Local Imports
const hashUtil = require('../../utils/hashUtils');
const {userModel} = require('./userSchema');
const hashUtil = require('../../utils/hashUtils.js');
const {userModel} = require('./userSchema.js');
const loggerUtils = require('../../utils/loggerUtils.js');
const userBanSchema = new mongoose.Schema({
user: {
@ -184,21 +185,21 @@ userBanSchema.statics.checkProcessedBans = async function(user){
userBanSchema.statics.banByUserDoc = async function(userDB, permanent, expirationDays, ipBan = false){
//Prevent missing users
if(userDB == null){
throw new Error("User not found")
throw loggerUtils.exceptionSmith("User not found", "validation");
}
//Ensure the user isn't already banned
if(await this.checkBanByUserDoc(userDB) != null){
throw new Error("User already banned");
throw loggerUtils.exceptionSmith("User already banned", "validation");
}
//Verify time to expire/delete depending on action
if(expirationDays < 0){
throw new Error("Expiration Days must be a positive integer!");
throw loggerUtils.exceptionSmith("Expiration Days must be a positive integer!", "validation");
}else if(expirationDays < 30 && permanent){
throw new Error("Permanent bans must be given at least 30 days before automatic account deletion!");
throw loggerUtils.exceptionSmith("Permanent bans must be given at least 30 days before automatic account deletion!", "validation");
}else if(expirationDays > 185){
throw new Error("Expiration/Deletion date cannot be longer than half a year out from the original ban date.");
throw loggerUtils.exceptionSmith("Expiration/Deletion date cannot be longer than half a year out from the original ban date.", "validation");
}
await banSessions(userDB);
@ -266,13 +267,13 @@ userBanSchema.statics.unbanByUserDoc = async function(userDB){
//Prevent missing users
if(userDB == null){
throw new Error("User not found")
throw loggerUtils.exceptionSmith("User not found", "validation");
}
const banDB = await this.checkBanByUserDoc(userDB);
if(!banDB){
throw new Error("User already un-banned");
throw loggerUtils.exceptionSmith("User already un-banned", "validation");
}
//Use _id in-case mongoose wants to be a cunt
@ -284,7 +285,7 @@ userBanSchema.statics.unbanDeleted = async function(user){
const banDB = await this.checkProcessedBans(user);
if(!banDB){
throw new Error("User already un-banned");
throw loggerUtils.exceptionSmith("User already un-banned", "validation");
}
const oldBan = await this.deleteOne({_id: banDB._id});

View file

@ -14,9 +14,6 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Node Imports
const { profile } = require('console');
//NPM Imports
const {mongoose} = require('mongoose');
@ -33,6 +30,7 @@ const playlistSchema = require('../channel/media/playlistSchema');
//Utils
const hashUtil = require('../../utils/hashUtils');
const mailUtil = require('../../utils/mailUtils');
const loggerUtils = require('../../utils/loggerUtils')
const userSchema = new mongoose.Schema({
@ -161,7 +159,7 @@ userSchema.pre('save', async function (next){
await this.populate('flair');
if(permissionModel.rankToNum(this.rank) < permissionModel.rankToNum(this.flair.rank)){
throw new Error(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`);
throw loggerUtils.exceptionSmith(`User '${this.user}' does not have a high enough rank for flair '${this.flair.displayName}'!`, "unauthorized");
}
}
@ -223,7 +221,7 @@ userSchema.statics.register = async function(userObj, ip){
//If the user is found or someones trying to impersonate tokeboi
if(userDB || user.toLowerCase() == "tokebot"){
throw new Error("User name/email already taken!");
throw loggerUtils.exceptionSmith("User name/email already taken!", "validation");
}else{
//Increment the user count, pulling the id to tattoo to the user
const id = await statModel.incrementUserCount();
@ -242,14 +240,14 @@ userSchema.statics.register = async function(userObj, ip){
}
}
}else{
throw new Error("Confirmation password doesn't match!");
throw loggerUtils.exceptionSmith("Confirmation password doesn't match!", "validation");
}
}
userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Username or Password."){
//check for missing pass
if(!user || !pass){
throw new Error("Missing user/pass.");
throw loggerUtils.exceptionSmith("Missing user/pass.", "validation");
}
//get the user if it exists
@ -268,9 +266,9 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use
badLogin();
}
//standardize bad login response so it's unknowin which is bad for security reasons.
//standardize bad login response so it's unknown which is bad for security reasons.
function badLogin(){
throw new Error(failLine);
throw loggerUtils.exceptionSmith(failLine, "unauthorized");
}
}
@ -688,11 +686,11 @@ userSchema.methods.changePassword = async function(passChange){
await this.killAllSessions("Your password has been reset.");
}else{
//confirmation pass doesn't match
throw new Error("Mismatched confirmation password!");
throw loggerUtils.exceptionSmith("Mismatched confirmation password!", "validation");
}
}else{
//Old password wrong
throw new Error("Incorrect Password!");
throw loggerUtils.exceptionSmith("Incorrect Password!", "validation");
}
}
@ -716,7 +714,7 @@ userSchema.methods.nuke = async function(pass){
//Check we have a confirmation password
if(pass == "" || pass == null){
//scream and shout
throw new Error("No confirmation password!");
throw loggerUtils.exceptionSmith("No confirmation password!", "validation");
}
//Check that the password is correct
@ -725,7 +723,7 @@ userSchema.methods.nuke = async function(pass){
var oldUser = await this.deleteOne();
}else{
//complain about a bad pass
throw new Error("Bad pass.");
throw loggerUtils.exceptionSmith("Bad pass.", "unauthorized");
}
}

View file

@ -17,7 +17,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Config
const config = require('../../config.json');
//At some point this will be a bit more advanced, right now it's just a placeholder :P
module.exports.exceptionSmith = function(msg, type){
//Create the new error with the given message
const exception = new Error(msg);
//Set the error type
exception.type = type;
//Mark the error as a custom error
exception.custom = true;
//Return the error
return exception;
}
module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){
//Some controllers do things after sending headers, for those, we should remain silent
if(!res.headersSent){
@ -35,11 +48,16 @@ module.exports.localExceptionHandler = function(err){
}
module.exports.exceptionHandler = function(res, err){
//If this is a self-made problem
if(err.custom){
module.exports.errorHandler(res, err.message, err.type);
}else{
//Locally handle the exception
module.exports.localExceptionHandler(err);
//if not yell at the browser for fucking up, and tell it what it did wrong.
module.exports.errorHandler(res, err.message, "Caught Exception");
module.exports.errorHandler(res, "An unexpected server crash was just prevented. You should probably report this to an admin.", "Caught Exception");
}
}
module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){
@ -47,16 +65,28 @@ module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){
}
module.exports.socketExceptionHandler = function(socket, err){
//If this is a self made problem
if(err.custom){
//at the browser for fucking up, and tell it what it did wrong.
return module.exports.socketErrorHandler(socket, err.message, err.type);
}else{
//Locally handle the exception
module.exports.localExceptionHandler(err);
//if not yell at the browser for fucking up, and tell it what it did wrong.
return module.exports.socketErrorHandler(socket, err.msg, "Caught Exception");
//if not yell at the browser for fucking up
return module.exports.socketErrorHandler(socket, "Caught Exception", "Caught Exception");
}
}
module.exports.socketCriticalExceptionHandler = function(socket, err){
//if not yell at the browser for fucking up, and tell it what it did wrong.
//If this is a self made problem
if(err.custom){
//yell at the browser for fucking up, and tell it what it did wrong.
socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`});
}else{
//yell at the browser for fucking up
socket.emit("kick", {type: "Disconnected", reason: "An unexpected server crash was just prevented. You should probably report this to an admin."});
}
return socket.disconnect();
}

View file

@ -19,8 +19,9 @@ const url = require("node:url");
const validator = require('validator');
//Local Imports
const regexUtils = require('../regexUtils');
const media = require('../../app/channel/media/media');
const media = require('../../app/channel/media/media.js');
const regexUtils = require('../regexUtils.js');
const loggerUtils = require('../loggerUtils.js')
module.exports.fetchMetadata = async function(link, title){
//Parse link
@ -52,7 +53,7 @@ module.exports.fetchMetadata = async function(link, title){
if(!response.ok){
//Scream and shout
const errorBody = await response.text();
throw new Error(`Internet Archive Error '${response.status}': ${errorBody}`);
throw loggerUtils.exceptionSmith(`Internet Archive Error '${response.status}': ${errorBody}`, "queue");
}
//Collect our metadata

View file

@ -45,7 +45,7 @@ module.exports.kickoff = function(){
//Process Hashed IP Records that haven't been recorded in a week or more
userModel.processAgedIPRecords();
//Process expired global bans that may have expired since last restart
userBanModel.processExpiredBans()
userBanModel.processExpiredBans();
//Process expired channel bans that may have expired since last restart
channelModel.processExpiredBans();
//Process expired password reset requests that may have expired since last restart

View file

@ -16,9 +16,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Local Imports
const config = require('../../config.json');
const {userModel} = require('../schemas/user/userSchema');
const userBanModel = require('../schemas/user/userBanSchema')
const altchaUtils = require('../utils/altchaUtils');
const {userModel} = require('../schemas/user/userSchema.js');
const userBanModel = require('../schemas/user/userBanSchema.js')
const altchaUtils = require('../utils/altchaUtils.js');
const loggerUtils = require('../utils/loggerUtils.js')
//Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database
//Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue.
@ -41,15 +42,24 @@ module.exports.authenticateSession = async function(user, pass, req){
//If this ip is randy bobandy
if(ipBanDB != null){
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
const expiration = ipBanDB.getDaysUntilExpiration() < 1 ? 0 : ipBanDB.getDaysUntilExpiration();
//If the ban is permanent
if(ipBanDB.permanent){
//tell it to fuck off
throw new Error(`The IP address you are trying to login from has been banned.`);
throw loggerUtils.exceptionSmith(`The IP address you are trying to login from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`, "unauthorized");
}else{
//tell it to fuck off
throw loggerUtils.exceptionSmith(`The IP address you are trying to login from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`, "unauthorized");
}
}
//If we have failed attempts
if(attempt != null){
//If we have more failed attempts than allowed
if(attempt.count > maxAttempts){
throw new Error("This account has been locked for at 24 hours due to a large amount of failed log-in attempts");
throw loggerUtils.exceptionSmith("This account has been locked for at 24 hours due to a large amount of failed log-in attempts", "unauthorized");
}
//If we're throttling logins
@ -57,9 +67,9 @@ module.exports.authenticateSession = async function(user, pass, req){
//Verification doesnt get sanatized or checked since that would most likely break the cryptography
//Since we've already got access to the request and dont need to import anything, why bother getting it from a parameter?
if(req.body.verification == null){
throw new Error("Verification failed!");
throw loggerUtils.exceptionSmith("Verification failed!", "unauthorized");
}else if(!altchaUtils.verify(req.body.verification, user)){
throw new Error("Verification failed!");
throw loggerUtils.exceptionSmith("Verification failed!", "");
}
}
}
@ -75,9 +85,9 @@ module.exports.authenticateSession = async function(user, pass, req){
//Make the number a little prettier despite the lack of precision since we're not doing calculations here :P
const expiration = userBanDB.getDaysUntilExpiration() < 1 ? 0 : userBanDB.getDaysUntilExpiration();
if(userBanDB.permanent){
throw new Error(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`);
throw loggerUtils.exceptionSmith(`Your account has been permanently banned, and will be nuked from the database in: ${expiration} day(s)`, "unauthorized");
}else{
throw new Error(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`);
throw loggerUtils.exceptionSmith(`Your account has been temporarily banned, and will be reinstated in: ${expiration} day(s)`, "unauthorized");
}
}