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){
//Locally handle the exception
module.exports.localExceptionHandler(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");
//if not yell at the browser for fucking up, and tell it what it did wrong.
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){
//Locally handle the exception
module.exports.localExceptionHandler(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.
socket.emit("kick", {type: "Disconnected", reason: `Server Error: ${err.message}`});
//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){
//tell it to fuck off
throw new Error(`The IP address you are trying to login from has been banned.`);
//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 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");
}
}