Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
301
backend/server.js
Normal file
301
backend/server.js
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue