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 . %>
+
+
+
+
+
+
+
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