Seperated out socket validation/authorization from channel mangement logic.

This commit is contained in:
rainbow napkin 2025-09-18 04:13:19 -04:00
parent d541dce8c4
commit 7da07c8717
4 changed files with 108 additions and 74 deletions

View file

@ -22,6 +22,7 @@ const channelModel = require('../../schemas/channel/channelSchema');
const emoteModel = require('../../schemas/emoteSchema'); const emoteModel = require('../../schemas/emoteSchema');
const {userModel} = require('../../schemas/user/userSchema'); const {userModel} = require('../../schemas/user/userSchema');
const userBanModel = require('../../schemas/user/userBanSchema'); const userBanModel = require('../../schemas/user/userBanSchema');
const socketUtils = require('../../utils/socketUtils');
const loggerUtils = require('../../utils/loggerUtils'); const loggerUtils = require('../../utils/loggerUtils');
const csrfUtils = require('../../utils/csrfUtils'); const csrfUtils = require('../../utils/csrfUtils');
const presenceUtils = require('../../utils/presenceUtils'); const presenceUtils = require('../../utils/presenceUtils');
@ -68,7 +69,7 @@ class channelManager{
async handleConnection(socket){ async handleConnection(socket){
try{ try{
//ensure unbanned ip and valid CSRF token //ensure unbanned ip and valid CSRF token
if(!(await this.validateSocket(socket))){ if(!(await socketUtils.validateSocket(socket))){
socket.disconnect(); socket.disconnect();
return; return;
} }
@ -76,7 +77,7 @@ class channelManager{
//Prevent logged out connections and authenticate socket //Prevent logged out connections and authenticate socket
if(socket.request.session.user != null){ if(socket.request.session.user != null){
//Authenticate socket //Authenticate socket
const userDB = await this.authSocket(socket); const userDB = await socketUtils.authSocket(socket);
//Get the active channel based on the socket //Get the active channel based on the socket
var {activeChan, chanDB} = await this.getActiveChan(socket); var {activeChan, chanDB} = await this.getActiveChan(socket);
@ -148,70 +149,6 @@ class channelManager{
} }
} }
/**
* Global server-side validation logic for new connections to any channel
* @param {Socket} socket - Requesting Socket
* @returns {Boolean} true on success
*/
async validateSocket(socket){
//If we're proxied use passthrough IP
const ip = config.proxied ? socket.handshake.headers['x-forwarded-for'] : socket.handshake.address;
//Look for ban by IP
const ipBanDB = await userBanModel.checkBanByIP(ip);
//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
socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`});
//Otherwise
}else{
//tell it to fuck off
socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`});
}
return false;
}
//Check for Cross-Site Request Forgery
if(!csrfUtils.isRequestValid(socket.request)){
socket.emit("kick", {type: "disconnected", reason: "Invalid CSRF Token!"});
return false;
}
return true;
}
/**
* Global server-side authorization logic for new connections to any channel
* @param {Socket} socket - Requesting Socket
* @returns {Mongoose.Document} - Authorized User Document upon success
*/
async authSocket(socket){
//Find the user in the Database since the session won't store enough data to fulfill our needs :P
const userDB = await userModel.findOne({user: socket.request.session.user.user});
if(userDB == null){
throw loggerUtils.exceptionSmith("User not found!", "unauthorized");
}
//Set socket user and channel values
socket.user = {
id: userDB.id,
user: userDB.user,
};
return userDB;
}
/** /**
* Gets active channel from a given socket * Gets active channel from a given socket
* @param {Socket} socket - Socket to check * @param {Socket} socket - Socket to check

View file

@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//local includes //local includes
const server = require('../server'); const server = require('../server');
const userSchema = require('../schemas/user/userSchema'); const {userModel} = require('../schemas/user/userSchema');
//User activity map to keep us from constantly reading off of the DB //User activity map to keep us from constantly reading off of the DB
let activityMap = new Map(); let activityMap = new Map();
@ -47,7 +47,7 @@ module.exports.getPresence = async function(user, userDB){
//If we wheren't handed a free user doc //If we wheren't handed a free user doc
if(userDB == null){ if(userDB == null){
//Pull one from the username //Pull one from the username
userDB = await userSchema.userModel.findOne({user: user}); userDB = await userModel.findOne({user: user});
} }
//If for some reason we can't find a user doc //If for some reason we can't find a user doc
@ -119,7 +119,7 @@ module.exports.handlePresence = async function(user, userDB, noSave = false){
//If we wheren't handed a free user doc //If we wheren't handed a free user doc
if(userDB == null){ if(userDB == null){
//Pull one from the username //Pull one from the username
userDB = await userSchema.userModel.findOne({user: user}); userDB = await userModel.findOne({user: user});
} }
//Set last active in user's DB document //Set last active in user's DB document

97
src/utils/socketUtils.js Normal file
View file

@ -0,0 +1,97 @@
/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
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/>.*/
const config = require('../../config.json');
const csrfUtils = require('./csrfUtils');
const {userModel} = require('../schemas/user/userSchema');
const userBanModel = require('../schemas/user/userBanSchema');
module.exports.validateSocket = async function(socket, quiet = false){
//If we're proxied use passthrough IP
const ip = config.proxied ? socket.handshake.headers['x-forwarded-for'] : socket.handshake.address;
//Look for ban by IP
const ipBanDB = await userBanModel.checkBanByIP(ip);
//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(quiet){
socket.disconnect();
}else{
//If the ban is permanent
if(ipBanDB.permanent){
//tell it to fuck off
socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been permanently banned. Your cleartext IP has been saved to the database. Any associated accounts will be nuked in ${expiration} day(s).`});
//Otherwise
}else{
//tell it to fuck off
socket.emit("kick", {type: "kicked", reason: `The IP address you are trying to connect from has been temporarily banned. Your cleartext IP has been saved to the database until the ban expires in ${expiration} day(s).`});
}
}
return false;
}
//Check for Cross-Site Request Forgery
if(!csrfUtils.isRequestValid(socket.request)){
if(quiet){
socket.disconnect();
}else{
socket.emit("kick", {type: "disconnected", reason: "Invalid CSRF Token!"});
}
return false;
}
return true;
}
//socket.request.session is already trusted, we don't actually need to verify against DB for authorzation
//It's just a useful place to grab the DB doc, and is mostly a stand-in from when the only socket-related code was in the channel folder
module.exports.authSocketLite = async function(socket){
const user = socket.request.session.user;
//Set socket user and channel values
socket.user = {
id: user.id,
user: user.user,
};
//return user object from session
return user;
}
module.exports.authSocket = async function(socket){
//Find the user in the Database since the session won't store enough data to fulfill our needs :P
const userDB = await userModel.findOne({user: socket.request.session.user.user});
if(userDB == null){
throw loggerUtils.exceptionSmith("User not found!", "unauthorized");
}
//Set socket user and channel values
socket.user = {
id: userDB.id,
user: userDB.user,
};
return userDB;
}

View file

@ -89,11 +89,11 @@ class channel{
this.socket.on("kick", async (data) => { this.socket.on("kick", async (data) => {
if(data.reason == "Invalid CSRF Token!"){ if(data.reason == "Invalid CSRF Token!"){
//Reload the CSRF token //Warn the user
await utils.ajax.reloadCSRFToken(); new canopyUXUtils.popup('Invalid CSRF Token detected, reloading client...');
//Retry the connection //Just reload the fucker
this.connect(); setTimeout(()=>{location.reload();}, 1000);
}else{ }else{
new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:<br>${data.reason}`); new canopyUXUtils.popup(`You have been ${data.type} from the channel for the following reason:<br>${data.reason}`);
} }