Authenticate browser media with durable HttpOnly login cookies
Adapted cookie, origin, URL validation and secret handling from MikaelKW/nodecast-tv-plus. Protect API roles, revoke logins across restart, preserve existing accounts, and isolate transcode sessions by account. Co-authored-by: Mikael <mikaelkwessel@gmail.com>
This commit is contained in:
@ -2,6 +2,10 @@ const express = require('express');
|
||||
require('dotenv').config();
|
||||
const path = require('path');
|
||||
const passport = require('passport');
|
||||
const auth = require('./auth');
|
||||
const security = require('./config/security');
|
||||
const { requestLimit } = require('./services/requestLimits');
|
||||
const { validateHttpUrl, redactText } = require('./services/urlSecurity');
|
||||
const syncService = require('./services/syncService');
|
||||
|
||||
// Initialize database
|
||||
@ -13,7 +17,8 @@ const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Trust proxy headers (X-Forwarded-Proto, X-Forwarded-For, etc.)
|
||||
// Required for correct protocol detection behind reverse proxies (nginx, Caddy, etc.)
|
||||
app.set('trust proxy', true);
|
||||
const trustProxy = process.env.TRUST_PROXY;
|
||||
app.set('trust proxy', trustProxy === 'true' ? true : trustProxy === 'false' ? false : trustProxy || 'loopback, linklocal, uniquelocal');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
@ -21,9 +26,11 @@ app.use(express.json({ limit: '50mb' }));
|
||||
// Initialize Passport
|
||||
const session = require('express-session');
|
||||
app.use(session({
|
||||
secret: process.env.JWT_SECRET || 'keyboard cat',
|
||||
name: security.sessionCookieName,
|
||||
secret: security.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: true
|
||||
saveUninitialized: false,
|
||||
cookie: { httpOnly: true, sameSite: 'lax', secure: 'auto', maxAge: 24 * 60 * 60 * 1000 }
|
||||
}));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
@ -172,7 +179,25 @@ process.on('SIGTERM', async () => {
|
||||
|
||||
// API Routes
|
||||
app.get('/api/health', require('./services/health'));
|
||||
app.get('/api/version', (req, res) => res.json({ version: require('../package.json').version }));
|
||||
app.use('/api', auth.requireSameOrigin);
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
app.use('/api', auth.requireAuth);
|
||||
app.use('/api', (req, res, next) => {
|
||||
try {
|
||||
if (req.query.url !== undefined) validateHttpUrl(req.query.url);
|
||||
if (req.body?.url !== undefined) validateHttpUrl(req.body.url);
|
||||
next();
|
||||
} catch (error) { res.status(400).json({ error: error.message }); }
|
||||
});
|
||||
app.use('/api', (req, res, next) => {
|
||||
const adminAction = !['GET', 'HEAD', 'OPTIONS'].includes(req.method)
|
||||
&& /^\/(sources|channels|settings|proxy\/cache)(\/|$)/.test(req.path);
|
||||
if (adminAction) return auth.requireAdmin(req, res, next);
|
||||
next();
|
||||
});
|
||||
const mediaLimit = requestLimit(60);
|
||||
app.use(['/api/remux', '/api/probe', '/api/subtitle'], mediaLimit);
|
||||
app.use('/api/sources', require('./routes/sources'));
|
||||
app.use('/api/proxy', require('./routes/proxy'));
|
||||
app.use('/api/channels', require('./routes/channels'));
|
||||
@ -184,12 +209,6 @@ app.use('/api/subtitle', require('./routes/subtitle'));
|
||||
app.use('/api/settings', require('./routes/settings'));
|
||||
app.use('/api/history', require('./routes/history'));
|
||||
|
||||
// Version endpoint
|
||||
app.get('/api/version', (req, res) => {
|
||||
const pkg = require('../package.json');
|
||||
res.json({ version: pkg.version });
|
||||
});
|
||||
|
||||
// SPA fallback - serve index.html for all non-API routes
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));
|
||||
@ -197,18 +216,19 @@ app.get('*', (req, res) => {
|
||||
|
||||
// Error handling
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Server error:', err);
|
||||
console.error('Server error:', redactText(err.message));
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
app.listen(PORT, async () => {
|
||||
console.log(`NodeCast TV server running on http://localhost:${PORT}`);
|
||||
const server = app.listen(PORT, async () => {
|
||||
console.log(`NodeCast TV server running on http://localhost:${server.address().port}`);
|
||||
|
||||
// Load plugins
|
||||
await loadPlugins().catch(err => {
|
||||
console.error('Plugin initialization failed:', err);
|
||||
});
|
||||
app.locals.ready = true;
|
||||
if (process.env.NODECAST_DISABLE_BACKGROUND_JOBS === 'true') return;
|
||||
|
||||
// Trigger background sync with delay to allow server to settle
|
||||
setTimeout(async () => {
|
||||
|
||||
Reference in New Issue
Block a user