diff --git a/src/controllers/404Controller.js b/src/controllers/404Controller.js new file mode 100644 index 0000000..5136b02 --- /dev/null +++ b/src/controllers/404Controller.js @@ -0,0 +1,30 @@ +/*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 .*/ + +//Config +const config = require('../../config.json'); + +//Local Imports +const csrfUtils = require('../utils/csrfUtils'); + +//register page functions +module.exports = async function(req, res, next){ + //set status + res.status(404); + + //Render page + return res.render('404', {instance: config.instanceName, user: req.session.user, csrfToken: csrfUtils.generateToken(req)}); +} \ No newline at end of file diff --git a/src/controllers/api/account/logoutController.js b/src/controllers/api/account/logoutController.js index 6a82ae4..0499469 100644 --- a/src/controllers/api/account/logoutController.js +++ b/src/controllers/api/account/logoutController.js @@ -18,7 +18,7 @@ along with this program. If not, see .*/ const accountUtils = require('../../../utils/sessionUtils'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); -module.exports.get = async function(req, res){ +module.exports.post = async function(req, res){ if(req.session.user){ try{ accountUtils.killSession(req.session); diff --git a/src/routers/api/accountRouter.js b/src/routers/api/accountRouter.js index 350c7e7..f066546 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -35,7 +35,7 @@ const router = Router(); //login router.post('/login', accountValidator.user(), accountValidator.pass(), loginController.post); //logout -router.get('/logout', logoutController.get); +router.post('/logout', logoutController.post); //register router.post('/register', accountValidator.user(), accountValidator.securePass(), diff --git a/src/routers/apiRouter.js b/src/routers/apiRouter.js index 3f56bf8..0512102 100644 --- a/src/routers/apiRouter.js +++ b/src/routers/apiRouter.js @@ -21,10 +21,14 @@ const { Router } = require('express'); const accountRouter = require("./api/accountRouter"); const channelRouter = require("./api/channelRouter"); const adminRouter = require("./api/adminRouter"); +const csrfUtil = require('../utils/csrfUtils'); //globals const router = Router(); +//Apply Cross-Site Request Forgery protection to API calls +router.use(csrfUtil.csrfSynchronisedProtection); + //routing functions router.use('/account', accountRouter); router.use('/channel', channelRouter); diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index ce3bcdc..e170704 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -409,10 +409,13 @@ userSchema.methods.getAuthenticatedSessions = async function(){ //crawl through active sessions sessions.forEach((session) => { - //if a session matches the current user - if(session.user.id == this.id){ - //we return it - returnArr.push(session); + //Skip un-authed sessions + if(session.user != null){ + //if a session matches the current user + if(session.user.id == this.id){ + //we return it + returnArr.push(session); + } } }); diff --git a/src/server.js b/src/server.js index 905658d..5a53293 100644 --- a/src/server.js +++ b/src/server.js @@ -32,11 +32,14 @@ const channelManager = require('./app/channel/channelManager'); //Util const configCheck = require('./utils/configCheck'); const scheduler = require('./utils/scheduler'); +const {errorMiddleware} = require('./utils/loggerUtils'); //DB Model const statModel = require('./schemas/statSchema'); const flairModel = require('./schemas/flairSchema'); const emoteModel = require('./schemas/emoteSchema'); const tokeCommandModel = require('./schemas/tokebot/tokeCommandSchema'); +//Controller +const fileNotFoundController = require('./controllers/404Controller'); //Router //Humie-Friendly const indexRouter = require('./routers/indexRouter'); @@ -132,6 +135,14 @@ app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altch //Server public 'www' folder app.use(express.static(path.join(__dirname, '../www'))); +//Handle error checking +app.use(errorMiddleware); + +//Basic 404 handler +app.use(fileNotFoundController); + + + //Increment launch counter statModel.incrementLaunchCount(); diff --git a/src/utils/csrfUtils.js b/src/utils/csrfUtils.js index de04476..e64667f 100644 --- a/src/utils/csrfUtils.js +++ b/src/utils/csrfUtils.js @@ -17,8 +17,11 @@ along with this program. If not, see .*/ //NPM Imports const { csrfSync } = require('csrf-sync'); +//Local Imports +const {errorHandler} = require('./loggerUtils'); + //Pull needed methods from csrfSync -const {generateToken, revokeToken, csrfSynchronisedProtection,} = csrfSync(); +const {generateToken, revokeToken, csrfSynchronisedProtection} = csrfSync(); //Export them per csrfSync documentation module.exports.generateToken = generateToken; diff --git a/src/utils/loggerUtils.js b/src/utils/loggerUtils.js index 38b7a21..a62b196 100644 --- a/src/utils/loggerUtils.js +++ b/src/utils/loggerUtils.js @@ -38,4 +38,17 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){ 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 +module.exports.errorMiddleware = function(err, req, res, next){ + //Set generic error + var reason = "Server Error"; + + //If it's un-authorized + if(err.status == 403){ + reason = "Unauthorized" + } + + module.exports.errorHandler(res, err.message, reason, err.status); } \ No newline at end of file diff --git a/src/views/404.ejs b/src/views/404.ejs new file mode 100644 index 0000000..227885d --- /dev/null +++ b/src/views/404.ejs @@ -0,0 +1,33 @@ +<%# 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 . %> + + + + <%- include('partial/styles', {instance, user}); %> + <%- include('partial/csrfToken', {csrfToken}); %> + + <%= instance %> + + + <%- include('partial/navbar', {user}); %> +

404

+

Congratulations, you've found a dead link!

+ + +
+ <%- include('partial/scripts', {user}); %> +
+ diff --git a/www/css/error.css b/www/css/error.css new file mode 100644 index 0000000..66af654 --- /dev/null +++ b/www/css/error.css @@ -0,0 +1,23 @@ +/*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 .*/ +h1, h3{ + text-align: center; +} + +img{ + width: 50%; + margin: 0 auto; +} \ No newline at end of file diff --git a/www/js/utils.js b/www/js/utils.js index 794a159..9852fe7 100644 --- a/www/js/utils.js +++ b/www/js/utils.js @@ -395,7 +395,9 @@ class canopyAjaxUtils{ var response = await fetch(`/api/account/register`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + //It's either this or find and bind all event listeners :P + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify(email ? {user, pass, passConfirm, email, verification} : {user, pass, passConfirm, verification}) }); @@ -411,7 +413,8 @@ class canopyAjaxUtils{ var response = await fetch(`/api/account/login`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify(verification ? {user, pass, verification} : {user, pass}) }); @@ -427,7 +430,10 @@ class canopyAjaxUtils{ async logout(){ var response = await fetch(`/api/account/logout`,{ - method: "GET", + method: "POST", + headers: { + "x-csrf-token": utils.ajax.getCSRFToken() + } }); if(response.status == 200){ @@ -441,7 +447,8 @@ class canopyAjaxUtils{ const response = await fetch(`/api/account/update`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify(update) }); @@ -469,7 +476,8 @@ class canopyAjaxUtils{ const response = await fetch(`/api/account/delete`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify({pass}) }); @@ -485,7 +493,8 @@ class canopyAjaxUtils{ const response = await fetch(`/api/account/passwordResetRequest`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify({user, verification}) }); @@ -506,7 +515,8 @@ class canopyAjaxUtils{ const response = await fetch(`/api/account/passwordReset`,{ method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "x-csrf-token": utils.ajax.getCSRFToken() }, body: JSON.stringify({token, pass, confirmPass, verification}) }); @@ -782,6 +792,11 @@ class canopyAjaxUtils{ } } + //Syntatic sugar + getCSRFToken(){ + return document.querySelector("[name='csrf-token']").content; + } + } const utils = new canopyUtils() \ No newline at end of file