Files
sofarr/server/index.js
Gronod b608fa0337 fix(security #12): add helmet security response headers
Adds X-DNS-Prefetch-Control, X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, X-XSS-Protection, HSTS (in prod) and others.
CSP disabled for now as the SPA uses inline scripts/styles; a
nonce/hash-based policy is a future hardening step.
2026-05-16 16:23:47 +01:00

97 lines
3.4 KiB
JavaScript

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const fs = require('fs');
require('dotenv').config();
// Setup logging with levels
// Levels: debug (0), info (1), warn (2), error (3), silent (4)
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
const currentLevel = LOG_LEVELS[(process.env.LOG_LEVEL || 'info').toLowerCase()] || 1;
const logFile = fs.createWriteStream(path.join(__dirname, '../server.log'), { flags: 'a' });
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleDebug = console.debug;
function shouldLog(level) {
return level >= currentLevel;
}
console.debug = function(...args) {
if (!shouldLog(LOG_LEVELS.debug)) return;
const message = args.join(' ');
originalConsoleDebug.apply(console, args);
logFile.write(`[${new Date().toISOString()}] DEBUG: ${message}\n`);
};
console.log = function(...args) {
if (!shouldLog(LOG_LEVELS.info)) return;
const message = args.join(' ');
originalConsoleLog.apply(console, args);
logFile.write(`[${new Date().toISOString()}] ${message}\n`);
};
console.warn = function(...args) {
if (!shouldLog(LOG_LEVELS.warn)) return;
const message = args.join(' ');
originalConsoleWarn.apply(console, args);
logFile.write(`[${new Date().toISOString()}] WARN: ${message}\n`);
};
console.error = function(...args) {
if (!shouldLog(LOG_LEVELS.error)) return;
const message = args.join(' ');
originalConsoleError.apply(console, args);
logFile.write(`[${new Date().toISOString()}] ERROR: ${message}\n`);
};
const sabnzbdRoutes = require('./routes/sabnzbd');
const sonarrRoutes = require('./routes/sonarr');
const radarrRoutes = require('./routes/radarr');
const embyRoutes = require('./routes/emby');
const dashboardRoutes = require('./routes/dashboard');
const authRoutes = require('./routes/auth');
const { startPoller, POLL_INTERVAL, POLLING_ENABLED } = require('./utils/poller');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(helmet({
contentSecurityPolicy: false // SPA uses inline scripts; CSP requires a nonce/hash strategy
}));
const cookieSecret = process.env.COOKIE_SECRET;
if (!cookieSecret && process.env.NODE_ENV === 'production') {
console.error('[Security] COOKIE_SECRET is not set in production — cookies are unsigned and can be tampered with!');
process.exit(1);
} else if (!cookieSecret) {
console.warn('[Security] COOKIE_SECRET is not set — using unsigned cookies (acceptable for development only)');
}
app.use(cookieParser(cookieSecret || undefined));
app.use(express.json());
app.use(express.static(path.join(__dirname, '../public')));
app.use('/api/sabnzbd', sabnzbdRoutes);
app.use('/api/sonarr', sonarrRoutes);
app.use('/api/radarr', radarrRoutes);
app.use('/api/emby', embyRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/auth', authRoutes);
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../public/index.html'));
});
app.listen(PORT, () => {
console.log(`=================================`);
console.log(` sofarr - Your Downloads Dashboard`);
console.log(` Server running on port ${PORT}`);
console.log(` Log level: ${process.env.LOG_LEVEL || 'info'}`);
console.log(` Polling: ${POLLING_ENABLED ? POLL_INTERVAL + 'ms' : 'disabled (on-demand)'}`);
console.log(`=================================`);
startPoller();
});