From 7b8c44158fd06cb07de94f3899037ae0365b43a1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 31 Aug 2025 03:44:23 -0400 Subject: [PATCH] Finished adding JSDoc for src/utils --- src/utils/altchaUtils.js | 22 +++++++-- src/utils/configCheck.js | 5 ++- src/utils/hashUtils.js | 18 ++++++++ src/utils/linkUtils.js | 8 ++++ src/utils/loggerUtils.js | 59 ++++++++++++++++++++++++- src/utils/mailUtils.js | 17 +++++++ src/utils/media/internetArchiveUtils.js | 6 +++ src/utils/media/yanker.js | 27 +++++++++-- src/utils/media/ytdlpUtils.js | 30 ++++++++++++- src/utils/regexUtils.js | 13 ++++-- src/utils/scheduler.js | 6 +++ src/utils/sessionUtils.js | 38 ++++++++++++++-- 12 files changed, 229 insertions(+), 20 deletions(-) diff --git a/src/utils/altchaUtils.js b/src/utils/altchaUtils.js index 78e06d8..4c7754f 100644 --- a/src/utils/altchaUtils.js +++ b/src/utils/altchaUtils.js @@ -20,11 +20,21 @@ const config = require('../../config.json'); //NPM imports 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 = []; -//Captcha lifetime in minutes +/** + * Captcha lifetime in minutes + */ 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 = ''){ //Set altcha expiration 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 = ''){ - //If we already checked this payload + //If this payload is already spent if(spent.indexOf(payload) != -1){ //Fuck off and die return false; diff --git a/src/utils/configCheck.js b/src/utils/configCheck.js index d0f9c55..a705195 100644 --- a/src/utils/configCheck.js +++ b/src/utils/configCheck.js @@ -23,7 +23,10 @@ const loggerUtil = require('./loggerUtils'); //NPM Imports 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(){ //Check Protocol if(config.protocol.toLowerCase() != 'https'){ diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index cc78d01..52616c6 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -23,15 +23,33 @@ const crypto = require('node:crypto'); //NPM Imports 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){ const salt = bcrypt.genSaltSync(); 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){ 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){ //Create hash object const hashObj = crypto.createHash('md5'); diff --git a/src/utils/linkUtils.js b/src/utils/linkUtils.js index 9708ab7..6452db1 100644 --- a/src/utils/linkUtils.js +++ b/src/utils/linkUtils.js @@ -18,8 +18,16 @@ along with this program. If not, see .*/ const validator = require('validator');//No express here, so regular validator it is! //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(); +/** + * 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){ //Check link cache for the requested link const cachedLink = module.exports.cache.get(link); diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index b6daae9..0189f05 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -17,6 +17,15 @@ along with this program. If not, see .*/ //Config 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){ //Create the new error with the given message const exception = new Error(msg); @@ -31,6 +40,14 @@ module.exports.exceptionSmith = function(msg, type){ 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){ //Some controllers do things after sending headers, for those, we should remain silent 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){ //If we're being 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){ //If this is a self-made problem 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"){ 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){ //If this is a self made problem 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); }else{ //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){ //If this is a self made problem if(err.custom){ @@ -93,11 +138,21 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){ return socket.disconnect(); } +/** + * Prints warning text to server console + * @param {String} string - String to print to console + */ module.exports.consoleWarn = function(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){ //Set generic error var reason = "Server Error"; diff --git a/src/utils/mailUtils.js b/src/utils/mailUtils.js index 29d744a..3df123b 100644 --- a/src/utils/mailUtils.js +++ b/src/utils/mailUtils.js @@ -21,6 +21,9 @@ const config = require('../../config.json'); const nodeMailer = require("nodemailer"); //Setup mail transport +/** + * nodemailer transport object, generated from options specific in our config file + */ const transporter = nodeMailer.createTransport({ host: config.mail.host, 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){ //Create mail object const mailObj = { @@ -56,6 +67,12 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){ 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){ //Send the reset url via email await module.exports.mailem( diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js index 71e2a7a..8f7debf 100644 --- a/src/utils/media/internetArchiveUtils.js +++ b/src/utils/media/internetArchiveUtils.js @@ -22,6 +22,12 @@ const media = require('../../app/channel/media/media.js'); const regexUtils = require('../regexUtils.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){ //Split fullID by first slash const [itemID, requestedPath] = decodeURIComponent(fullID).split(/\/(.*)/); diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 1aaef04..5ff5c38 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -22,6 +22,12 @@ const validator = require('validator');//No express here, so regular validator i const iaUtil = require('./internetArchiveUtils'); 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){ //Get pull type 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){ switch(mediaObj.type){ 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; } - -//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 +/** + * Detects media type by URL + * + * 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){ //Check if we have a valid url, encode it on the fly in case it's too humie-friendly if(!validator.isURL(encodeURI(url))){ diff --git a/src/utils/media/ytdlpUtils.js b/src/utils/media/ytdlpUtils.js index c487b86..075189a 100644 --- a/src/utils/media/ytdlpUtils.js +++ b/src/utils/media/ytdlpUtils.js @@ -29,6 +29,12 @@ const media = require('../../app/channel/media/media.js'); const regexUtils = require('../regexUtils.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){ try{ //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){ try{ //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){ //Pull media from dailymotion link const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); //Return found 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'){ //Create media list 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 +/** + * 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'){ //return promise from ytdlp return ytdlp(link, { diff --git a/src/utils/regexUtils.js b/src/utils/regexUtils.js index 09a50c4..64be99a 100644 --- a/src/utils/regexUtils.js +++ b/src/utils/regexUtils.js @@ -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 along with this program. If not, see .*/ +/** + * 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){ - /* 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, '\\$&'); } \ No newline at end of file diff --git a/src/utils/scheduler.js b/src/utils/scheduler.js index 44a8dd9..f1a09bc 100644 --- a/src/utils/scheduler.js +++ b/src/utils/scheduler.js @@ -26,6 +26,9 @@ const channelModel = require('../schemas/channel/channelSchema'); const sessionUtils = require('./sessionUtils'); const { email } = require('../validators/accountValidator'); +/** + * Schedules all timed jobs accross the server + */ module.exports.schedule = function(){ //Process hashed IP Records that haven't been recorded in a week or more 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"}); } +/** + * Kicks off first run of scheduled functions before scheduling functions for regular callback + */ module.exports.kickoff = function(){ //Process Hashed IP Records that haven't been recorded in a week or more userModel.processAgedIPRecords(); diff --git a/src/utils/sessionUtils.js b/src/utils/sessionUtils.js index 818a1ae..6aeae3b 100644 --- a/src/utils/sessionUtils.js +++ b/src/utils/sessionUtils.js @@ -19,15 +19,33 @@ const config = require('../../config.json'); 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') +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(); + +/** + * How many failed attempts required to throttle with altcha + */ const throttleAttempts = 5; + +/** + * How many attempts to lock user account out for the day + */ 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){ //Fuck you yoda 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){ 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){ //Read the code, i'm not explaining this 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(){ for(user of failedAttempts.keys()){ //Get attempt by user