const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const compression = require('compression'); const path = require('path'); const dotenv = require('dotenv'); const fileUpload = require('express-fileupload'); const crypto = require('crypto'); const logger = require('./utils/logger'); const { errorMiddleware, ErrorResponses } = require('./utils/errorHandler'); const db = require('./database/db'); const logManagement = require('./jobs/logManagement'); dotenv.config(); const app = express(); const PORT = process.env.PORT || 12345; const isProduction = process.env.NODE_ENV === 'production'; // Generate nonce for inline scripts app.use((req, res, next) => { res.locals.nonce = crypto.randomBytes(16).toString('base64'); next(); }); // Security middleware with comprehensive CSP app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: [ "'self'", "'unsafe-inline'", // Required for React/Vite inline scripts "'unsafe-eval'", // Required for React DevTools and some libraries "https://www.gstatic.com", // Google Cast SDK "https://cdn.jsdelivr.net", // HLS.js library "blob:", // Required for HLS.js Web Workers (req, res) => `'nonce-${res.locals.nonce}'` ], workerSrc: [ "'self'", "blob:" // Required for HLS.js Web Workers ], styleSrc: [ "'self'", "'unsafe-inline'", // Required for MUI and inline styles "https://fonts.googleapis.com" ], fontSrc: [ "'self'", "data:", "https://fonts.gstatic.com" ], imgSrc: [ "'self'", "data:", "blob:", "https:", "http:" // Allow external logo URLs ], mediaSrc: [ "'self'", "blob:", "data:", "mediastream:", "https:", "http:", // Required for IPTV streams "*" // Allow all media sources for streaming ], connectSrc: [ "'self'", "https:", "http:", "ws:", "wss:", // WebSocket support "blob:", "*" // Required for external APIs and streams ], frameSrc: [ "'self'", "https://www.youtube.com", // For embedded players "https://player.vimeo.com" ], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'self'"] // upgradeInsecureRequests disabled for HTTP-only deployments }, reportOnly: !isProduction, // Report-only mode in development useDefaults: false }, originAgentCluster: false, // Disable to avoid agent cluster warnings crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false, crossOriginResourcePolicy: { policy: "cross-origin" }, hsts: isProduction ? { maxAge: 31536000, includeSubDomains: true, preload: true } : false, referrerPolicy: { policy: "strict-origin-when-cross-origin" }, noSniff: true, xssFilter: true, hidePoweredBy: true })); // CORS configuration to allow local network and HTTPS domain const allowedOrigins = [ 'http://localhost:12345', 'http://localhost:9000', 'https://tv.iulian.uk', 'http://tv.iulian.uk', /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}(:\d+)?$/, // Local network 192.168.x.x /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/, // Local network 10.x.x.x /^http:\/\/172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}(:\d+)?$/ // Local network 172.16-31.x.x ]; // Mount logo-proxy BEFORE global CORS to handle public image serving app.use('/api/logo-proxy', require('./routes/logo-proxy')); app.use(cors({ origin: function (origin, callback) { // Allow requests with no origin (mobile apps, curl, etc.) if (!origin) return callback(null, true); // Check if origin matches allowed patterns const isAllowed = allowedOrigins.some(allowed => { if (typeof allowed === 'string') { return origin === allowed; } else if (allowed instanceof RegExp) { return allowed.test(origin); } return false; }); if (isAllowed) { callback(null, true); } else { console.warn(`[CORS] Rejected origin: ${origin}`); callback(new Error('Not allowed by CORS')); } }, credentials: true, // Allow cookies and authentication headers methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] })); app.use(compression()); app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); app.use(fileUpload({ limits: { fileSize: 100 * 1024 * 1024 }, // 100MB max file size useTempFiles: true, tempFileDir: '/tmp/' })); // Initialize database db.initialize(); // Serve static files (uploaded logos) app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Serve cached logos from IPTV-org app.use('/logos', express.static(path.join('/app', 'data', 'logo-cache'))); // Health check endpoint (no rate limiting) app.get('/api/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // API Routes app.use('/api/auth', require('./routes/auth')); app.use('/api/users', require('./routes/users')); app.use('/api/playlists', require('./routes/playlists')); app.use('/api/channels', require('./routes/channels')); app.use('/api/recordings', require('./routes/recordings')); app.use('/api/two-factor', require('./routes/twoFactor')); app.use('/api/profiles', require('./routes/profiles')); app.use('/api/radio', require('./routes/radio')); app.use('/api/groups', require('./routes/groups')); app.use('/api/settings', require('./routes/settings')); app.use('/api/stream', require('./routes/stream')); app.use('/api/stats', require('./routes/stats')); app.use('/api/m3u-files', require('./routes/m3u-files')); app.use('/api/vpn-configs', require('./routes/vpn-configs')); app.use('/api/search', require('./routes/search')); app.use('/api/favorites', require('./routes/favorites')); app.use('/api/backup', require('./routes/backup')); app.use('/api/metadata', require('./routes/metadata')); app.use('/api/history', require('./routes/history')); app.use('/api/logo-cache', require('./routes/logo-cache')); app.use('/api/sessions', require('./routes/sessions')); app.use('/api/csp', require('./routes/csp')); app.use('/api/rbac', require('./routes/rbac')); app.use('/api/security-monitor', require('./routes/security-monitor')); app.use('/api/security-headers', require('./routes/security-headers')); app.use('/api/security-testing', require('./routes/security-testing')); app.use('/api/security-config', require('./routes/security-config')); app.use('/api/siem', require('./routes/siem')); app.use('/api/log-management', require('./routes/log-management')); app.use('/api/encryption', require('./routes/encryption-management')); // Serve static files from frontend app.use(express.static(path.join(__dirname, '../frontend/dist'))); // Serve uploaded files app.use('/uploads', express.static(path.join(__dirname, '../data/uploads'))); app.use('/logos', express.static(path.join(__dirname, '../data/logos'))); app.use('/data/logo-cache', express.static(path.join(__dirname, '../data/logo-cache'))); // Handle SPA routing (must be before error handler) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); }); // Secure error handling middleware (CWE-209 protection) // This MUST be the last middleware - catches all errors and sanitizes them app.use(errorMiddleware); // Reset VPN states on startup (connections are lost on restart) db.db.run('UPDATE vpn_configs SET is_active = 0', (err) => { if (err) { logger.error('Failed to reset VPN states on startup:', err); } else { logger.info('Reset all VPN connection states on startup'); } }); // Start background jobs require('./jobs/channelHealth'); require('./jobs/recordingScheduler'); require('./jobs/logoCacher'); // Process-level error handlers (CWE-391 protection) // Handle uncaught exceptions process.on('uncaughtException', (error) => { logger.error('UNCAUGHT EXCEPTION - Process will exit', { error: error.message, stack: error.stack, timestamp: new Date().toISOString() }); // Give logger time to write setTimeout(() => { process.exit(1); }, 1000); }); // Handle unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { logger.error('UNHANDLED PROMISE REJECTION', { reason: reason instanceof Error ? reason.message : reason, stack: reason instanceof Error ? reason.stack : undefined, promise: promise.toString(), timestamp: new Date().toISOString() }); }); // Handle SIGTERM gracefully process.on('SIGTERM', () => { logger.info('SIGTERM received, closing server gracefully'); // Close database connections, clear intervals, etc. db.db.close((err) => { if (err) { logger.error('Error closing database:', err); } process.exit(0); }); }); // Handle SIGINT (Ctrl+C) gracefully process.on('SIGINT', () => { logger.info('SIGINT received, closing server gracefully'); process.exit(0); }); // Start server const server = app.listen(PORT, '0.0.0.0', () => { logger.info(`StreamFlow server running on port ${PORT}`); console.log(`StreamFlow server running on port ${PORT}`); // Initialize log management system (CWE-53 compliance) logManagement.initialize().catch(err => { logger.error('Failed to initialize log management:', err); }); }); // Handle server errors server.on('error', (error) => { logger.error('Server error:', { error: error.message, code: error.code, stack: error.stack }); if (error.code === 'EADDRINUSE') { logger.error(`Port ${PORT} is already in use`); process.exit(1); } }); module.exports = app;