Finished adding JSDoc for src/utils

This commit is contained in:
rainbow napkin 2025-08-31 03:44:23 -04:00
parent 2303c89bcf
commit 7b8c44158f
12 changed files with 229 additions and 20 deletions

View file

@ -20,11 +20,21 @@ const config = require('../../config.json');
//NPM imports //NPM imports
const { createChallenge, verifySolution } = require('altcha-lib'); const { createChallenge, verifySolution } = require('altcha-lib');
//Create empty array to hold cache of spent payloades to protect against replay attacks /**
* Create empty array to hold cache of spent payloads to protect against replay attacks
*/
const spent = []; const spent = [];
//Captcha lifetime in minutes /**
* Captcha lifetime in minutes
*/
const lifetime = 2; const lifetime = 2;
/**
* Generates captcha challenges to send down to the browser
* @param {Number} difficulty - Challange Difficulty (x100K internally)
* @param {String} uniqueSecret - Secret to salt the challange hash with
* @returns {String} Altcha Challenge hash
*/
module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){ module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){
//Set altcha expiration date //Set altcha expiration date
const expiration = new Date(); const expiration = new Date();
@ -40,8 +50,14 @@ module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){
}); });
} }
/**
* Verifies completed altcha challenges handed over from the user
* @param {String} payload - Completed Altcha Payload
* @param {String} uniqueSecret - Server-side Unique Secret to verify payload came from server-generated challenge
* @returns {boolean} True if payload is a valid and unique altcha challenge which originated from this server
*/
module.exports.verify = async function(payload, uniqueSecret = ''){ module.exports.verify = async function(payload, uniqueSecret = ''){
//If we already checked this payload //If this payload is already spent
if(spent.indexOf(payload) != -1){ if(spent.indexOf(payload) != -1){
//Fuck off and die //Fuck off and die
return false; return false;

View file

@ -23,7 +23,10 @@ const loggerUtil = require('./loggerUtils');
//NPM Imports //NPM Imports
const validator = require('validator');//We need validators for express-less code too! const validator = require('validator');//We need validators for express-less code too!
/**
* Basic security check which runs on startup.
* Warns server admin against unsafe config options.
*/
module.exports.securityCheck = function(){ module.exports.securityCheck = function(){
//Check Protocol //Check Protocol
if(config.protocol.toLowerCase() != 'https'){ if(config.protocol.toLowerCase() != 'https'){

View file

@ -23,15 +23,33 @@ const crypto = require('node:crypto');
//NPM Imports //NPM Imports
const bcrypt = require('bcrypt'); const bcrypt = require('bcrypt');
/**
* Sitewide function for hashing passwords
* @param {String} pass - Password to hash
* @returns {String} Hashed/Salted password
*/
module.exports.hashPassword = function(pass){ module.exports.hashPassword = function(pass){
const salt = bcrypt.genSaltSync(); const salt = bcrypt.genSaltSync();
return bcrypt.hashSync(pass, salt); return bcrypt.hashSync(pass, salt);
} }
/**
* Sitewide password for authenticating/comparing passwords agianst hashes
* @param {String} pass - Plaintext Password
* @param {String} hash - Salty Hash
* @returns {Boolean} True if authentication success
*/
module.exports.comparePassword = function(pass, hash){ module.exports.comparePassword = function(pass, hash){
return bcrypt.compareSync(pass, hash); return bcrypt.compareSync(pass, hash);
} }
/**
* Site-wide IP hashing/salting function
*
* Provides a basic level of privacy by only logging salted hashes of IP's
* @param {String} ip - IP to hash
* @returns {String} Hashed/Salted IP Adress
*/
module.exports.hashIP = function(ip){ module.exports.hashIP = function(ip){
//Create hash object //Create hash object
const hashObj = crypto.createHash('md5'); const hashObj = crypto.createHash('md5');

View file

@ -18,8 +18,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const validator = require('validator');//No express here, so regular validator it is! const validator = require('validator');//No express here, so regular validator it is!
//Create link cache //Create link cache
/**
* Basic RAM-Based cache of links, so we don't have to re-pull things after we get them
*/
module.exports.cache = new Map(); module.exports.cache = new Map();
/**
* Validates links and returns a marked link object that can be returned to the client to format/embed accordingly
* @param {String} link - URL to Validate
* @returns {Object} Marked link object
*/
module.exports.markLink = async function(link){ module.exports.markLink = async function(link){
//Check link cache for the requested link //Check link cache for the requested link
const cachedLink = module.exports.cache.get(link); const cachedLink = module.exports.cache.get(link);

View file

@ -17,6 +17,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//Config //Config
const config = require('../../config.json'); const config = require('../../config.json');
/**
* Creates and returns a custom exception, tagged as a 'custom' exception, using the 'custom' boolean property.
* This is used to denote that this error was generated on purpose, with a human readable message, that can be securely sent to the client.
* Unexpected exceptions should only be logged internally, however, as they may contain sensitive data.
*
* @param {String} msg - Error message to send the client
* @param {String} type - Error type to send back to the client
* @returns {Error} The exception to smith
*/
module.exports.exceptionSmith = function(msg, type){ module.exports.exceptionSmith = function(msg, type){
//Create the new error with the given message //Create the new error with the given message
const exception = new Error(msg); const exception = new Error(msg);
@ -31,6 +40,14 @@ module.exports.exceptionSmith = function(msg, type){
return exception; return exception;
} }
/**
* Main error handling function
* @param {Express.Response} res - Response being sent out to the client who caused the issue
* @param {String} msg - Error message to send the client
* @param {String} type - Error type to send back to the client
* @param {Number} status - HTTP(s) Status Code to send back to the client
* @returns {Express.Response} If we have a usable Express Response object, return it back after it's been cashed
*/
module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){ module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){
//Some controllers do things after sending headers, for those, we should remain silent //Some controllers do things after sending headers, for those, we should remain silent
if(!res.headersSent){ if(!res.headersSent){
@ -39,6 +56,10 @@ module.exports.errorHandler = function(res, msg, type = "Generic", status = 400)
} }
} }
/**
* Handles local exceptions which where not directly created by user interaction
* @param {Error} err - Exception to handle
*/
module.exports.localExceptionHandler = function(err){ module.exports.localExceptionHandler = function(err){
//If we're being verbose //If we're being verbose
if(config.verbose){ if(config.verbose){
@ -47,6 +68,11 @@ module.exports.localExceptionHandler = function(err){
} }
} }
/**
* Handles exceptions which where directly the fault of user action >:(
* @param {Express.Response} res - Express Response object to bitch at
* @param {Error} err - Error created by the jerk in question
*/
module.exports.exceptionHandler = function(res, err){ module.exports.exceptionHandler = function(res, err){
//If this is a self-made problem //If this is a self-made problem
if(err.custom){ if(err.custom){
@ -60,14 +86,27 @@ module.exports.exceptionHandler = function(res, err){
} }
} }
/**
* Basic error-handling for socket.io so we don't just silently swallow errors.
* @param {Socket} socket - Socket error originated from
* @param {String} msg - Error message to send the client
* @param {String} type - Error type to send back to the client
* @returns {Boolean} - Passthrough from socket.emit
*/
module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){ module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){
return socket.emit("error", {errors: [{type, msg, date: new Date()}]}); return socket.emit("error", {errors: [{type, msg, date: new Date()}]});
} }
/**
* Generates error messages for simple errors generated by socket.io interaction
* @param {Socket} socket - Socket error originated from
* @param {Error} err - Error created by the jerk in question
* @returns {Boolean} - Passthrough from socket.emit
*/
module.exports.socketExceptionHandler = function(socket, err){ module.exports.socketExceptionHandler = function(socket, err){
//If this is a self made problem //If this is a self made problem
if(err.custom){ if(err.custom){
//at the browser for fucking up, and tell it what it did wrong. //yell at the browser for fucking up, and tell it what it did wrong.
return module.exports.socketErrorHandler(socket, err.message, err.type); return module.exports.socketErrorHandler(socket, err.message, err.type);
}else{ }else{
//Locally handle the exception //Locally handle the exception
@ -78,6 +117,12 @@ module.exports.socketExceptionHandler = function(socket, err){
} }
} }
/**
* Generates error messages and drops connection for critical errors caused by socket.io interaction
* @param {Socket} socket - Socket error originated from
* @param {Error} err - Error created by the jerk in question
* @returns {Boolean} - Passthrough from socket.disconnect
*/
module.exports.socketCriticalExceptionHandler = function(socket, err){ module.exports.socketCriticalExceptionHandler = function(socket, err){
//If this is a self made problem //If this is a self made problem
if(err.custom){ if(err.custom){
@ -93,11 +138,21 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){
return socket.disconnect(); return socket.disconnect();
} }
/**
* Prints warning text to server console
* @param {String} string - String to print to console
*/
module.exports.consoleWarn = function(string){ module.exports.consoleWarn = function(string){
console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string); console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string);
} }
//Basic error-handling middleware to ensure we're not dumping stack traces /**
* Basic error-handling middleware to ensure we're not dumping stack traces to the client, as that would be insecure
* @param {Error} err - Error to handle
* @param {Express.Request} req - Express Request
* @param {Express.Response} res - Express Response
* @param {Function} next - Next function in the Express middleware chain (Not that it's getting called XP)
*/
module.exports.errorMiddleware = function(err, req, res, next){ module.exports.errorMiddleware = function(err, req, res, next){
//Set generic error //Set generic error
var reason = "Server Error"; var reason = "Server Error";

View file

@ -21,6 +21,9 @@ const config = require('../../config.json');
const nodeMailer = require("nodemailer"); const nodeMailer = require("nodemailer");
//Setup mail transport //Setup mail transport
/**
* nodemailer transport object, generated from options specific in our config file
*/
const transporter = nodeMailer.createTransport({ const transporter = nodeMailer.createTransport({
host: config.mail.host, host: config.mail.host,
port: config.mail.port, port: config.mail.port,
@ -31,6 +34,14 @@ const transporter = nodeMailer.createTransport({
} }
}); });
/**
* Sends an email as tokebot to the requested user w/ the requested body and signature
* @param {String} to - String containing the email address to send to
* @param {String} subject - Subject line of the email to send
* @param {String} body - Body contents, either HTML or Plaintext
* @param {Boolean} htmlBody - Whether or not Body contents should be sent as HTML or Plaintext
* @returns {Object} Sent mail info
*/
module.exports.mailem = async function(to, subject, body, htmlBody = false){ module.exports.mailem = async function(to, subject, body, htmlBody = false){
//Create mail object //Create mail object
const mailObj = { const mailObj = {
@ -56,6 +67,12 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){
return sentMail; return sentMail;
} }
/**
* Sends address verification email
* @param {Mongoose.Document} requestDB - DB Document Object for the current email change request token
* @param {Mongoose.Document} userDB - DB Document Object for the user we're verifying email against
* @param {String} newEmail - New email address to send to
*/
module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail){ module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail){
//Send the reset url via email //Send the reset url via email
await module.exports.mailem( await module.exports.mailem(

View file

@ -22,6 +22,12 @@ const media = require('../../app/channel/media/media.js');
const regexUtils = require('../regexUtils.js'); const regexUtils = require('../regexUtils.js');
const loggerUtils = require('../loggerUtils.js') const loggerUtils = require('../loggerUtils.js')
/**
* Pulls metadate for a given archive.org item
* @param {String} fullID - Full path of the requested upload
* @param {String} title - Title to add to media object
* @returns {Array} Generated list of media objects from given upload path
*/
module.exports.fetchMetadata = async function(fullID, title){ module.exports.fetchMetadata = async function(fullID, title){
//Split fullID by first slash //Split fullID by first slash
const [itemID, requestedPath] = decodeURIComponent(fullID).split(/\/(.*)/); const [itemID, requestedPath] = decodeURIComponent(fullID).split(/\/(.*)/);

View file

@ -22,6 +22,12 @@ const validator = require('validator');//No express here, so regular validator i
const iaUtil = require('./internetArchiveUtils'); const iaUtil = require('./internetArchiveUtils');
const ytdlpUtil = require('./ytdlpUtils'); const ytdlpUtil = require('./ytdlpUtils');
/**
* Checks a given URL and runs the proper metadata fetching function to create a media object from any supported URL
* @param {String} url - URL to yank media against
* @param {String} title - Title to apply to yanked media
* @returns
*/
module.exports.yankMedia = async function(url, title){ module.exports.yankMedia = async function(url, title){
//Get pull type //Get pull type
const pullType = await this.getMediaType(url); const pullType = await this.getMediaType(url);
@ -50,6 +56,13 @@ module.exports.yankMedia = async function(url, title){
} }
} }
/**
* Refreshes raw links on relevant media objects
*
* Useful for sources like youtube, who only provide expiring raw links
* @param {ScheduledMedia} mediaObj - Media Object to refresh
* @returns {ScheduledMedia} Refreshed media object
*/
module.exports.refreshRawLink = async function(mediaObj){ module.exports.refreshRawLink = async function(mediaObj){
switch(mediaObj.type){ switch(mediaObj.type){
case 'yt': case 'yt':
@ -76,10 +89,16 @@ module.exports.refreshRawLink = async function(mediaObj){
//Return null to tell the calling function there is no refresh required for this media type //Return null to tell the calling function there is no refresh required for this media type
return null; return null;
} }
/**
//I'd be lying if this didn't take at least some inspiration/regex patterns from extractQueryParam() in cytube/forest's browser-side 'util.js' * Detects media type by URL
//Still this has some improvements like url pre-checks and the fact that it's handled serverside, recuing possibility of bad requests. *
//Some of the regex expressions for certain services have also been improved, such as youtube, and the fore.st-unique archive.org * I'd be lying if this didn't take at least some inspiration/regex patterns from extractQueryParam() in cytube/forest's browser-side 'util.js'
* Still this has some improvements like url pre-checks and the fact that it's handled serverside, recuing possibility of bad requests.
* Some of the regex expressions for certain services have also been improved, such as youtube, and the fore.st-unique archive.org
*
* @param {String} url - URL to determine media type of
* @returns {Object} containing URL type and clipped ID string
*/
module.exports.getMediaType = async function(url){ module.exports.getMediaType = async function(url){
//Check if we have a valid url, encode it on the fly in case it's too humie-friendly //Check if we have a valid url, encode it on the fly in case it's too humie-friendly
if(!validator.isURL(encodeURI(url))){ if(!validator.isURL(encodeURI(url))){

View file

@ -29,6 +29,12 @@ const media = require('../../app/channel/media/media.js');
const regexUtils = require('../regexUtils.js'); const regexUtils = require('../regexUtils.js');
const loggerUtils = require('../loggerUtils.js') const loggerUtils = require('../loggerUtils.js')
/**
* Pulls metadata for a single youtube video via YT-DLP
* @param {String} id - Youtube Video ID
* @param {String} title - Title to add to the given media object
* @returns {Media} Media object containing relevant metadata
*/
module.exports.fetchYoutubeMetadata = async function(id, title){ module.exports.fetchYoutubeMetadata = async function(id, title){
try{ try{
//Try to pull media from youtube id //Try to pull media from youtube id
@ -50,6 +56,12 @@ module.exports.fetchYoutubeMetadata = async function(id, title){
} }
} }
/**
* Pulls metadata for a playlist of youtube videos via YT-DLP
* @param {String} id - Youtube Playlist ID
* @param {String} title - Title to add to the given media objects
* @returns {Array} Array of Media objects containing relevant metadata
*/
module.exports.fetchYoutubePlaylistMetadata = async function(id, title){ module.exports.fetchYoutubePlaylistMetadata = async function(id, title){
try{ try{
//Try to pull media from youtube id //Try to pull media from youtube id
@ -71,15 +83,23 @@ module.exports.fetchYoutubePlaylistMetadata = async function(id, title){
} }
} }
/* This requires HLS embeds which, in-turn, require daily motion to add us to their CORS exception list
* Not gonna happen, so we need to use their API for this, or proxy the video
module.exports.fetchDailymotionMetadata = async function(id, title){ module.exports.fetchDailymotionMetadata = async function(id, title){
//Pull media from dailymotion link //Pull media from dailymotion link
const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm');
//Return found media; //Return found media;
return media; return media;
} }*/
//Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata /**
* Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata
* @param {String} link - Link to video in question
* @param {String} title - Title to add to the given media objects
* @param {String} type - Link type to attach to the resulting media object
* @returns {Array} Array of Media objects containing relevant metadata
*/
async function fetchVideoMetadata(link, title, type, format = 'b'){ async function fetchVideoMetadata(link, title, type, format = 'b'){
//Create media list //Create media list
const mediaList = []; const mediaList = [];
@ -110,6 +130,12 @@ async function fetchVideoMetadata(link, title, type, format = 'b'){
}*/ }*/
//Wrapper function for YT-DLP NPM package with pre-set cli-flags //Wrapper function for YT-DLP NPM package with pre-set cli-flags
/**
* Basic async YT-DLP Fetch wrapper, ensuring config
* @param {String} link - Link to fetch using YT-DLP
* @param {String} format - Format string to hand YT-DLP, defaults to 'b'
* @returns {Object} Metadata dump from YT-DLP
*/
async function ytdlpFetch(link, format = 'b'){ async function ytdlpFetch(link, format = 'b'){
//return promise from ytdlp //return promise from ytdlp
return ytdlp(link, { return ytdlp(link, {

View file

@ -14,10 +14,15 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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/>.*/ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
/**
* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
* In my defense I only did it because js-runtime-devs are taking fucking eons to implement RegExp.escape()
* This should be replaced once that function becomes available in mainline versions of node.js:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape
*
* @param {String} string - Regex string to escape
* @returns {String} The Escaped String
*/
module.exports.escapeRegex = function(string){ module.exports.escapeRegex = function(string){
/* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
In my defense I only did it because js-runtime-devs are taking fucking eons to implement RegExp.escape()
This should be replaced once that function becomes available in mainline versions of node.js:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} }

View file

@ -26,6 +26,9 @@ const channelModel = require('../schemas/channel/channelSchema');
const sessionUtils = require('./sessionUtils'); const sessionUtils = require('./sessionUtils');
const { email } = require('../validators/accountValidator'); const { email } = require('../validators/accountValidator');
/**
* Schedules all timed jobs accross the server
*/
module.exports.schedule = function(){ module.exports.schedule = function(){
//Process hashed IP Records that haven't been recorded in a week or more //Process hashed IP Records that haven't been recorded in a week or more
cron.schedule('0 0 * * *', ()=>{userModel.processAgedIPRecords()},{scheduled: true, timezone: "UTC"}); cron.schedule('0 0 * * *', ()=>{userModel.processAgedIPRecords()},{scheduled: true, timezone: "UTC"});
@ -41,6 +44,9 @@ module.exports.schedule = function(){
cron.schedule('0 0 * * *', ()=>{emailChangeModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"}); cron.schedule('0 0 * * *', ()=>{emailChangeModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"});
} }
/**
* Kicks off first run of scheduled functions before scheduling functions for regular callback
*/
module.exports.kickoff = function(){ module.exports.kickoff = function(){
//Process Hashed IP Records that haven't been recorded in a week or more //Process Hashed IP Records that haven't been recorded in a week or more
userModel.processAgedIPRecords(); userModel.processAgedIPRecords();

View file

@ -19,15 +19,33 @@ const config = require('../../config.json');
const {userModel} = require('../schemas/user/userSchema.js'); const {userModel} = require('../schemas/user/userSchema.js');
const userBanModel = require('../schemas/user/userBanSchema.js') const userBanModel = require('../schemas/user/userBanSchema.js')
const altchaUtils = require('../utils/altchaUtils.js'); const altchaUtils = require('../utils/altchaUtils.js');
const loggerUtils = require('../utils/loggerUtils.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. * 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.
*/
const failedAttempts = new Map(); const failedAttempts = new Map();
/**
* How many failed attempts required to throttle with altcha
*/
const throttleAttempts = 5; const throttleAttempts = 5;
/**
* How many attempts to lock user account out for the day
*/
const maxAttempts = 200; const maxAttempts = 200;
//this module is good for keeping wrappers for userModel and other shit in that does more session handling than database access/modification. /**
* Sole and Singular Session Authentication method.
* All logins should happen through here, all other site-wide authentication should happen by sessions authenticated by this model.
* This is important, as reducing authentication endpoints reduces attack surface.
* @param {String} user - Username to login as
* @param {String} pass - Password to authenticat session with
* @param {express.Request} req - Express request object w/ session to authenticate
* @returns Username of authticated user upon success
*/
module.exports.authenticateSession = async function(user, pass, req){ module.exports.authenticateSession = async function(user, pass, req){
//Fuck you yoda //Fuck you yoda
try{ try{
@ -137,15 +155,27 @@ module.exports.authenticateSession = async function(user, pass, req){
} }
} }
/**
* Logs user out and destroys all server-side traces of a given session
* @param {express-session.session} session
*/
module.exports.killSession = async function(session){ module.exports.killSession = async function(session){
session.destroy(); session.destroy();
} }
/**
* Returns how many failed login attempts within the past day or so since the last login has occured for a given user
* @param {String} user - User to check map against
* @returns {Number} of failed login attempts
*/
module.exports.getLoginAttempts = function(user){ module.exports.getLoginAttempts = function(user){
//Read the code, i'm not explaining this //Read the code, i'm not explaining this
return failedAttempts.get(user); return failedAttempts.get(user);
} }
/**
* Nightly Function Call which iterates through the failed login attempts map, removing any which haven't been attempted in over a da yeahy
*/
module.exports.processExpiredAttempts = function(){ module.exports.processExpiredAttempts = function(){
for(user of failedAttempts.keys()){ for(user of failedAttempts.keys()){
//Get attempt by user //Get attempt by user