File size: 7,540 Bytes
20c5151 98ace4c 20c5151 98ace4c 20c5151 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 7860;
// Detect Hugging Face environment and configure accordingly
const isHuggingFace = process.env.SPACE_ID || process.env.SPACE_AUTHOR_NAME ||
process.env.HF_SPACE || process.env.GRADIO_SERVER_NAME ||
(process.env.HOST && process.env.HOST.includes('hf.space'));
console.log(`🌍 Environment: ${isHuggingFace ? 'Hugging Face' : 'Local/Other'}`);
// Secure CSP configuration
const cspDirectives = {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline needed for inline event handlers
imgSrc: ["'self'", "https:", "data:", "blob:"],
frameSrc: ["'self'", "https://www.youtube-nocookie.com", "https://www.youtube.com"],
connectSrc: ["'self'"],
mediaSrc: ["'self'", "https:", "data:"],
objectSrc: ["'none'"],
childSrc: ["'self'", "https://www.youtube-nocookie.com"],
workerSrc: ["'self'", "blob:"],
manifestSrc: ["'self'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: isHuggingFace ? [] : null,
};
if (isHuggingFace) {
console.log('🔒 Hugging Face detected - Using balanced CSP for compatibility and security');
// Add Hugging Face specific domains
cspDirectives.frameSrc.push("https://*.hf.space", "https://*.huggingface.co");
cspDirectives.connectSrc.push("https://*.hf.space", "https://*.huggingface.co");
} else {
console.log('🔒 Local environment - Using strict CSP');
}
app.use(helmet({
contentSecurityPolicy: {
directives: cspDirectives,
reportOnly: false // Set to true for testing, false for enforcement
},
crossOriginEmbedderPolicy: false, // Disabled for YouTube embeds
crossOriginResourcePolicy: { policy: "cross-origin" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
strictTransportSecurity: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
xContentTypeOptions: true,
xDnsPrefetchControl: { allow: false },
xDownloadOptions: true,
xFrameOptions: { action: isHuggingFace ? 'sameorigin' : 'deny' },
xPoweredBy: false,
xXssProtection: true
}));
// Configure CORS based on environment
// Whitelist of allowed origins
const allowedOrigins = [
'http://localhost:7860',
'http://localhost:3000',
'https://*.hf.space',
'https://*.huggingface.co'
];
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, curl, etc.)
if (!origin) return callback(null, true);
if (isHuggingFace) {
// For Hugging Face, allow all hf.space and huggingface.co origins
if (origin.includes('hf.space') || origin.includes('huggingface.co')) {
return callback(null, true);
}
}
// Allow local network IPs (192.168.x.x, 10.x.x.x, etc.)
if (origin.match(/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/)) {
return callback(null, true);
}
// Check if origin is in whitelist
const isAllowed = allowedOrigins.some(allowed => {
if (allowed.includes('*')) {
const pattern = allowed.replace(/\*/g, '.*');
return new RegExp(pattern).test(origin);
}
return allowed === origin;
});
if (isAllowed) {
callback(null, true);
} else {
console.log(`Blocked origin: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['Content-Range', 'X-Content-Range'],
maxAge: 600 // Cache preflight requests for 10 minutes
};
app.use(cors(corsOptions));
// Additional security headers
app.use((req, res, next) => {
res.header('X-Content-Type-Options', 'nosniff');
res.header('X-Frame-Options', isHuggingFace ? 'SAMEORIGIN' : 'DENY');
res.header('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
// ⚡ Compressão otimizada com Gzip/Brotli
app.use(compression({
// Comprimir todos os responses acima de 1KB
threshold: 1024,
// Nível de compressão (0-9, 6 é o padrão balanceado)
level: 6,
// Filtrar tipos de conteúdo que devem ser comprimidos
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
}
}));
app.use(express.json());
// 📦 Servir arquivos estáticos com cache agressivo
app.use(express.static(path.join(__dirname, 'public'), {
// Cache por 1 ano para assets com versão
maxAge: '1y',
// Habilitar ETags
etag: true,
// Habilitar Last-Modified
lastModified: true,
// Configurar cache específico por tipo de arquivo
setHeaders: (res, path, stat) => {
// Cache agressivo para assets versionados (JS, CSS com hash)
if (path.match(/\.(js|css)$/)) {
res.set('Cache-Control', 'public, max-age=31536000, immutable');
}
// Cache moderado para HTML (1 hora)
else if (path.endsWith('.html')) {
res.set('Cache-Control', 'public, max-age=3600, must-revalidate');
}
// Cache longo para imagens
else if (path.match(/\.(jpg|jpeg|png|gif|svg|webp|ico)$/)) {
res.set('Cache-Control', 'public, max-age=604800'); // 1 semana
}
// Cache para fontes
else if (path.match(/\.(woff|woff2|ttf|eot)$/)) {
res.set('Cache-Control', 'public, max-age=31536000'); // 1 ano
}
// Cache para manifest e service worker (1 dia)
else if (path.match(/\.(json|webmanifest)$/) || path.endsWith('sw.js')) {
res.set('Cache-Control', 'public, max-age=86400'); // 1 dia
}
}
}));
// Routes
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// API endpoints for progress tracking
app.post('/api/progress', (req, res) => {
// In a real app, this would save to a database
res.json({ success: true, message: 'Progress saved' });
});
app.get('/api/progress/:day', (req, res) => {
// In a real app, this would fetch from a database
res.json({ day: req.params.day, completed: [] });
});
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 30-Day Keto Planner running on port ${PORT}`);
console.log(`📱 Access at: http://localhost:${PORT}`);
console.log(`✅ Server status: RUNNING`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('📴 SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('💤 Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('📴 SIGINT received, shutting down gracefully');
server.close(() => {
console.log('💤 Process terminated');
process.exit(0);
});
});
// Handle server errors
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ Port ${PORT} is already in use`);
console.log('💡 For local development, try a different port or stop other services');
console.log('🚀 For Hugging Face deployment, port 7860 will work correctly');
} else {
console.error('❌ Server error:', err);
}
process.exit(1);
});
|