streamflow/backend/server.js
2025-12-17 00:42:43 +00:00

301 lines
9.7 KiB
JavaScript

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;