cors
This commit is contained in:
@@ -17,6 +17,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
app.set('trust proxy', true);
|
app.set('trust proxy', true);
|
||||||
|
|
||||||
// Security middleware with relaxed CSP for SPA
|
// Security middleware with relaxed CSP for SPA
|
||||||
|
// Note: CSP is relaxed to allow assets to load properly
|
||||||
app.use(
|
app.use(
|
||||||
helmet({
|
helmet({
|
||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
@@ -26,20 +27,26 @@ app.use(
|
|||||||
"'self'",
|
"'self'",
|
||||||
"'unsafe-inline'", // Required for Vite HMR and some inline scripts
|
"'unsafe-inline'", // Required for Vite HMR and some inline scripts
|
||||||
"'unsafe-eval'", // Required for Vite dev mode
|
"'unsafe-eval'", // Required for Vite dev mode
|
||||||
|
"https:", // Allow scripts from HTTPS sources
|
||||||
|
"http:", // Allow scripts from HTTP sources (for development)
|
||||||
],
|
],
|
||||||
styleSrc: [
|
styleSrc: [
|
||||||
"'self'",
|
"'self'",
|
||||||
"'unsafe-inline'", // Required for inline styles
|
"'unsafe-inline'", // Required for inline styles
|
||||||
|
"https:", // Allow styles from HTTPS sources
|
||||||
|
"http:", // Allow styles from HTTP sources (for development)
|
||||||
],
|
],
|
||||||
imgSrc: ["'self'", "data:", "https:"],
|
imgSrc: ["'self'", "data:", "https:", "http:"],
|
||||||
fontSrc: ["'self'", "data:", "https:"],
|
fontSrc: ["'self'", "data:", "https:", "http:"],
|
||||||
connectSrc: ["'self'", "https:", "http:", "ws:", "wss:"], // Allow API calls
|
connectSrc: ["'self'", "https:", "http:", "ws:", "wss:"], // Allow API calls
|
||||||
frameSrc: ["'none'"],
|
frameSrc: ["'none'"],
|
||||||
objectSrc: ["'none'"],
|
objectSrc: ["'none'"],
|
||||||
// upgradeInsecureRequests removed - causes issues with reverse proxy
|
// upgradeInsecureRequests removed - causes issues with reverse proxy
|
||||||
|
// Instead, we rely on the reverse proxy to handle HTTPS
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
crossOriginEmbedderPolicy: false, // Disable for better compatibility
|
crossOriginEmbedderPolicy: false, // Disable for better compatibility
|
||||||
|
crossOriginResourcePolicy: { policy: "cross-origin" }, // Allow cross-origin resources
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,14 +182,37 @@ if (fs.existsSync(frontendDistPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serve static files with proper headers for SPA
|
// Serve static files with proper headers for SPA
|
||||||
// Use middleware to set headers with access to request object
|
// Middleware to set CORS and security headers for all static assets
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// Set CORS headers for assets if needed
|
// Get the origin from request (for CORS)
|
||||||
const origin = req.headers.origin;
|
const origin = req.headers.origin;
|
||||||
|
const protocol = req.protocol; // 'http' or 'https' (respects X-Forwarded-Proto)
|
||||||
|
const host = req.get('host');
|
||||||
|
|
||||||
|
// Set CORS headers for assets (allow same-origin and configured origins)
|
||||||
if (origin) {
|
if (origin) {
|
||||||
|
// Check if origin is allowed
|
||||||
|
const allowedOrigins = getAllowedOrigins();
|
||||||
|
if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(host)) {
|
||||||
res.setHeader('Access-Control-Allow-Origin', origin);
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Same-origin request (no origin header), allow it
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security headers for all static assets
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
res.setHeader('X-Frame-Options', 'DENY');
|
||||||
|
|
||||||
|
// Cache control for assets
|
||||||
|
if (req.path.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/)) {
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,15 +220,20 @@ if (fs.existsSync(frontendDistPath)) {
|
|||||||
maxAge: '1y', // Cache static assets
|
maxAge: '1y', // Cache static assets
|
||||||
etag: true,
|
etag: true,
|
||||||
lastModified: true,
|
lastModified: true,
|
||||||
setHeaders: (res, filePath) => {
|
setHeaders: (res, filePath, stat) => {
|
||||||
// Set proper content type for JS/CSS files
|
// Set proper content type for JS/CSS files
|
||||||
if (filePath.endsWith('.js')) {
|
if (filePath.endsWith('.js')) {
|
||||||
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
||||||
} else if (filePath.endsWith('.css')) {
|
} else if (filePath.endsWith('.css')) {
|
||||||
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
||||||
|
} else if (filePath.endsWith('.html')) {
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CORS headers are set (in case middleware didn't catch it)
|
||||||
|
if (!res.getHeader('Access-Control-Allow-Origin')) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
}
|
}
|
||||||
// Security headers for assets
|
|
||||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
@@ -262,16 +297,57 @@ if (fs.existsSync(frontendDistPath)) {
|
|||||||
logger.info(`SPA fallback: serving index.html for ${req.path}`);
|
logger.info(`SPA fallback: serving index.html for ${req.path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set proper headers for index.html
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// Set CORS headers for index.html if needed
|
||||||
|
const origin = req.headers.origin;
|
||||||
|
if (origin) {
|
||||||
|
const allowedOrigins = getAllowedOrigins();
|
||||||
|
if (isDevelopment || allowedOrigins.includes(origin) || origin.includes(req.get('host'))) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
||||||
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent caching of index.html (assets are cached, but HTML should not be)
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
|
||||||
// Serve frontend SPA
|
// Serve frontend SPA
|
||||||
res.sendFile(indexHtmlPath, (err) => {
|
// Read the file and potentially rewrite HTTP URLs to HTTPS if needed
|
||||||
|
fs.readFile(indexHtmlPath, 'utf8', (err, html) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error(`❌ Failed to send index.html: ${err.message}`);
|
logger.error(`❌ Failed to read index.html: ${err.message}`);
|
||||||
logger.error(`Error stack: ${err.stack}`);
|
logger.error(`Error stack: ${err.stack}`);
|
||||||
res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to load frontend',
|
error: 'Failed to load frontend',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the request is HTTPS (via reverse proxy), rewrite HTTP asset URLs to HTTPS
|
||||||
|
// This fixes mixed content issues
|
||||||
|
const protocol = req.protocol; // Will be 'https' if X-Forwarded-Proto is set correctly
|
||||||
|
if (protocol === 'https') {
|
||||||
|
// Replace absolute HTTP URLs with HTTPS (but keep relative URLs as-is)
|
||||||
|
html = html.replace(/href="http:\/\/([^"]+)"/g, (match, url) => {
|
||||||
|
if (url.includes(req.get('host'))) {
|
||||||
|
return `href="https://${url}"`;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
html = html.replace(/src="http:\/\/([^"]+)"/g, (match, url) => {
|
||||||
|
if (url.includes(req.get('host'))) {
|
||||||
|
return `src="https://${url}"`;
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user