// server.js - PRODUCTION-HARDENED VERSION WITH SECURITY import 'dotenv/config'; import express from 'express'; import http from 'http'; import { WebSocketServer } from 'ws'; import path from 'path'; import helmet from 'helmet'; import cors from 'cors'; import { fileURLToPath } from 'url'; import compression from 'compression'; import crypto from 'crypto'; import fs from 'fs'; import config from './config/config.js'; import { apiLimiter, memberLimiter } from './middleware/rateLimiter.js'; import requestLogger from './middleware/requestLogger.js'; import apiRoutes from './routes/api.js'; import memberRoutes from './routes/member.js'; import { createLogger } from './utils/logger.js'; import fileStorage from './utils/fileStorage.js'; import imageProxy from './services/imageProxy.js'; // ============================================================================ // ENVIRONMENT DETECTION // ============================================================================ const IS_RAILWAY = !!process.env.RAILWAY_PROJECT_ID; const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const env = (process.env.NODE_ENV || '').trim().toLowerCase(); const IS_DEVELOPMENT = env === 'development' || env === 'dev'; process.env.LOG_TO_FILE = IS_PRODUCTION ? 'true' : (process.env.LOG_TO_FILE || 'false'); // ============================================================================ // SECURITY MIDDLEWARE & UTILITIES // ============================================================================ class SecurityManager { constructor() { this.logger = createLogger('SECURITY'); this.blockedIPs = new Set(); this.suspiciousActivity = new Map(); // IP -> { count, firstSeen, lastSeen } this.failedAttempts = new Map(); // IP -> { count, lastAttempt } this.rateLimitStore = new Map(); // IP -> { requests: [], blocked: false } // Threat detection thresholds this.THRESHOLDS = { MAX_FAILED_ATTEMPTS: 5, FAILED_ATTEMPT_WINDOW: 15 * 60 * 1000, // 15 minutes SUSPICIOUS_SCORE_LIMIT: 10, MAX_REQUESTS_PER_MINUTE: 60, MAX_WS_CONNECTIONS_PER_IP: 5, BLOCK_DURATION: 60 * 60 * 1000, // 1 hour AUTO_UNBLOCK_CHECK: 10 * 60 * 1000 // 10 minutes }; // Malicious patterns this.MALICIOUS_PATTERNS = [ /(\.\.|\/\.\.)/g, // Path traversal /]*>.*?<\/script>/gi, // XSS /javascript:/gi, // XSS /on\w+\s*=/gi, // Event handlers /(\bor\b|\band\b).*?(\=|like)/gi, // SQL injection /union.*select/gi, // SQL injection /exec\s*\(/gi, // Code execution /eval\s*\(/gi, // Code execution /(rm|wget|curl)\s+-/gi, // Shell commands /\$\{.*\}/g, // Template injection /__proto__|constructor|prototype/gi // Prototype pollution ]; this.startCleanupInterval(); } // Block IP address blockIP(ip, reason, duration = this.THRESHOLDS.BLOCK_DURATION) { this.blockedIPs.add(ip); this.logger.warn('BLOCK', `IP blocked: ${ip}`, { reason, duration }); fileStorage.saveAnalytics('ip_blocked', { ip, reason, duration, timestamp: new Date().toISOString() }); // Auto-unblock after duration setTimeout(() => { this.blockedIPs.delete(ip); this.logger.info('UNBLOCK', `IP unblocked: ${ip}`); }, duration); } // Check if IP is blocked isBlocked(ip) { return this.blockedIPs.has(ip); } // Record failed attempt recordFailedAttempt(ip, reason) { const now = Date.now(); const record = this.failedAttempts.get(ip) || { count: 0, lastAttempt: now }; // Reset if outside window if (now - record.lastAttempt > this.THRESHOLDS.FAILED_ATTEMPT_WINDOW) { record.count = 0; } record.count++; record.lastAttempt = now; this.failedAttempts.set(ip, record); this.logger.warn('FAILED_ATTEMPT', `Failed attempt from ${ip}`, { reason, count: record.count }); // Block if threshold exceeded if (record.count >= this.THRESHOLDS.MAX_FAILED_ATTEMPTS) { this.blockIP(ip, `Too many failed attempts: ${reason}`); return true; } return false; } // Record suspicious activity recordSuspiciousActivity(ip, activity, score = 1) { const now = Date.now(); const record = this.suspiciousActivity.get(ip) || { count: 0, score: 0, firstSeen: now, lastSeen: now, activities: [] }; record.count++; record.score += score; record.lastSeen = now; record.activities.push({ activity, score, timestamp: now }); // Keep only last 50 activities if (record.activities.length > 50) { record.activities = record.activities.slice(-50); } this.suspiciousActivity.set(ip, record); this.logger.warn('SUSPICIOUS', `Suspicious activity from ${ip}`, { activity, score: record.score }); fileStorage.saveAnalytics('suspicious_activity', { ip, activity, score: record.score, timestamp: new Date().toISOString() }); // Block if score too high if (record.score >= this.THRESHOLDS.SUSPICIOUS_SCORE_LIMIT) { this.blockIP(ip, `Suspicious activity score: ${record.score}`); return true; } return false; } // Scan for malicious input scanForThreats(input) { const threats = []; const inputStr = typeof input === 'object' ? JSON.stringify(input) : String(input); for (const pattern of this.MALICIOUS_PATTERNS) { const matches = inputStr.match(pattern); if (matches) { threats.push({ pattern: pattern.source, matches: matches.slice(0, 3) // Limit to first 3 matches }); } } return threats; } // Validate input validateInput(input, maxLength = 1000) { if (typeof input !== 'string') return true; // Check length if (input.length > maxLength) { return false; } // Scan for threats const threats = this.scanForThreats(input); return threats.length === 0; } // Rate limiting check checkRateLimit(ip) { const now = Date.now(); const record = this.rateLimitStore.get(ip) || { requests: [], blocked: false }; // Remove old requests (older than 1 minute) record.requests = record.requests.filter(time => now - time < 60000); // Add current request record.requests.push(now); this.rateLimitStore.set(ip, record); // Check if exceeded if (record.requests.length > this.THRESHOLDS.MAX_REQUESTS_PER_MINUTE) { if (!record.blocked) { this.recordSuspiciousActivity(ip, 'Rate limit exceeded', 3); record.blocked = true; } return false; } record.blocked = false; return true; } // Sanitize input sanitize(input) { if (typeof input !== 'string') return input; return input .replace(/[<>]/g, '') // Remove angle brackets .replace(/javascript:/gi, '') .replace(/on\w+\s*=/gi, '') .trim() .slice(0, 1000); // Max length } // Cleanup old records startCleanupInterval() { setInterval(() => { const now = Date.now(); let cleaned = 0; // Clean failed attempts for (const [ip, record] of this.failedAttempts.entries()) { if (now - record.lastAttempt > this.THRESHOLDS.FAILED_ATTEMPT_WINDOW) { this.failedAttempts.delete(ip); cleaned++; } } // Clean suspicious activity for (const [ip, record] of this.suspiciousActivity.entries()) { if (now - record.lastSeen > 24 * 60 * 60 * 1000) { // 24 hours this.suspiciousActivity.delete(ip); cleaned++; } } // Clean rate limit store for (const [ip, record] of this.rateLimitStore.entries()) { if (record.requests.length === 0) { this.rateLimitStore.delete(ip); cleaned++; } } if (cleaned > 0) { this.logger.info('CLEANUP', `Cleaned ${cleaned} security records`); } }, this.THRESHOLDS.AUTO_UNBLOCK_CHECK); } // Get security stats getStats() { return { blockedIPs: Array.from(this.blockedIPs), totalBlocked: this.blockedIPs.size, suspiciousIPs: this.suspiciousActivity.size, failedAttempts: this.failedAttempts.size, topThreats: this.getTopThreats() }; } getTopThreats(limit = 10) { return Array.from(this.suspiciousActivity.entries()) .sort((a, b) => b[1].score - a[1].score) .slice(0, limit) .map(([ip, data]) => ({ ip, score: data.score, count: data.count })); } } const security = new SecurityManager(); // ============================================================================ // INPUT VALIDATION SCHEMAS // ============================================================================ const validators = { searchQuery: (q) => { if (!q || typeof q !== 'string') return false; if (q.length < 1 || q.length > 200) return false; if (!/^[a-zA-Z0-9\s\-.,()]+$/.test(q)) return false; return true; }, sessionId: (id) => { if (!id || typeof id !== 'string') return false; return /^(sess|ws)_\d+_[a-f0-9]{16}$/.test(id); }, numericParam: (val, min = 1, max = 365) => { const num = parseInt(val); return !isNaN(num) && num >= min && num <= max; }, path: (p) => { if (!p || typeof p !== 'string') return false; // Prevent path traversal return !p.includes('..') && !/[<>:"|?*]/.test(p); } }; // ============================================================================ // SECURITY MIDDLEWARE // ============================================================================ // IP blocking middleware const ipBlocker = (req, res, next) => { const ip = getClientIP(req); if (security.isBlocked(ip)) { this.logger.warn(req.sessionId, 'Blocked IP attempted access', { ip }); return res.status(403).json({ error: 'Access denied', code: 'IP_BLOCKED' }); } next(); }; // Rate limiting middleware (additional layer) const advancedRateLimit = (req, res, next) => { const ip = getClientIP(req); if (!security.checkRateLimit(ip)) { this.logger.warn(req.sessionId, 'Rate limit exceeded', { ip }); return res.status(429).json({ error: 'Too many requests', code: 'RATE_LIMIT_EXCEEDED', retryAfter: 60 }); } next(); }; // Input validation middleware const validateRequest = (req, res, next) => { const ip = getClientIP(req); // Scan query parameters for (const [key, value] of Object.entries(req.query)) { const threats = security.scanForThreats(value); if (threats.length > 0) { security.recordSuspiciousActivity(ip, `Malicious query parameter: ${key}`, 5); this.logger.warn(req.sessionId, 'Malicious input detected in query', { ip, key, threats }); return res.status(400).json({ error: 'Invalid request', code: 'INVALID_INPUT' }); } } // Scan request body if (req.body && typeof req.body === 'object') { const threats = security.scanForThreats(req.body); if (threats.length > 0) { security.recordSuspiciousActivity(ip, 'Malicious request body', 5); this.logger.warn(req.sessionId, 'Malicious input detected in body', { ip, threats }); return res.status(400).json({ error: 'Invalid request', code: 'INVALID_INPUT' }); } } next(); }; // Path traversal protection const pathTraversalProtection = (req, res, next) => { const ip = getClientIP(req); const url = req.originalUrl || req.url; if (url.includes('..') || url.includes('%2e%2e')) { security.recordSuspiciousActivity(ip, 'Path traversal attempt', 8); this.logger.warn(req.sessionId, 'Path traversal attempt', { ip, url }); return res.status(400).json({ error: 'Invalid request', code: 'INVALID_PATH' }); } next(); }; // WebSocket connection limiter const wsConnectionLimiter = new Map(); // IP -> connection count // ============================================================================ // CLEANUP SCHEDULER // ============================================================================ const cleanupScheduler = { logger: createLogger('CLEANUP'), logIntervalId: null, storageIntervalId: null, start() { if (!config.cleanup.enabled) { this.logger.info('INIT', 'Automatic cleanup disabled'); return; } this.logger.info('INIT', 'Starting cleanup scheduler', { logInterval: '1 hour', storageInterval: '24 hours', retention: config.cleanup.retention }); setTimeout(() => { this.runLogCleanup(); this.runStorageCleanup(); }, 5000); const logInterval = 1 * 60 * 60 * 1000; this.logIntervalId = setInterval(() => this.runLogCleanup(), logInterval); const storageInterval = 24 * 60 * 60 * 1000; this.storageIntervalId = setInterval(() => this.runStorageCleanup(), storageInterval); }, stop() { if (this.logIntervalId) clearInterval(this.logIntervalId); if (this.storageIntervalId) clearInterval(this.storageIntervalId); this.logger.info('STOPPED', 'Cleanup scheduler stopped'); }, async runLogCleanup() { try { this.logger.info('START', 'Running log cleanup'); const logCleanupResult = this.cleanupLogs(); this.logger.success('COMPLETE', 'Log cleanup finished', { retentionDays: config.cleanup.retention.logs, result: logCleanupResult }); return logCleanupResult; } catch (error) { this.logger.error('FAILED', 'Log cleanup failed', error); return { error: error.message }; } }, async runStorageCleanup() { try { this.logger.info('START', 'Running storage cleanup'); const storageCleanupResult = fileStorage.cleanupOldFiles(config.cleanup.retention); this.logger.success('COMPLETE', 'Storage cleanup finished', { totalDeleted: storageCleanupResult.totalDeleted, categories: Object.keys(storageCleanupResult.categories).length }); return storageCleanupResult; } catch (error) { this.logger.error('FAILED', 'Storage cleanup failed', error); return { error: error.message }; } }, cleanupLogs() { try { const loggerInstance = createLogger('TEMP'); loggerInstance.cleanupOldLogs(); return { success: true, retentionDays: config.cleanup.retention.logs, message: 'Log cleanup completed' }; } catch (error) { this.logger.error('LOGS', 'Failed to cleanup logs', error); return { success: false, error: error.message }; } } }; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const server = http.createServer(app); const wss = new WebSocketServer({ server }); // ============================================================================ // LOGGER INITIALIZATION // ============================================================================ const logger = createLogger('SERVER', { writeToFile: process.env.LOG_TO_FILE === 'true', consoleOutput: true }); const wsLogger = createLogger('WEBSOCKET'); const memLogger = createLogger('MEMORY'); // ============================================================================ // DIRECTORY SETUP // ============================================================================ const ensureDirectories = () => { const dirs = [ path.join(__dirname, 'logs'), path.join(__dirname, 'data'), path.join(__dirname, 'storage'), ]; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); logger.info('INIT', `Created directory: ${dir}`); } }); }; ensureDirectories(); // ============================================================================ // MEMORY MONITORING // ============================================================================ const MEMORY_CHECK_INTERVAL = IS_RAILWAY ? 15 * 60 * 1000 : 5 * 60 * 1000; const MAX_MEMORY_RECORDS = 1000; class MemoryMonitor { constructor() { this.records = []; this.metadata = { createdAt: new Date().toISOString(), platform: IS_RAILWAY ? 'Railway' : 'Local', nodeVersion: process.version, environment: process.env.NODE_ENV || 'production' }; this.highMemoryAlerted = false; } record() { const memUsage = process.memoryUsage(); const cpuUsage = process.cpuUsage(); const record = { timestamp: new Date().toISOString(), uptime: Math.round(process.uptime()), memory: { rss: Math.round(memUsage.rss / 1024 / 1024), heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), external: Math.round(memUsage.external / 1024 / 1024), arrayBuffers: Math.round(memUsage.arrayBuffers / 1024 / 1024) }, cpu: { user: Math.round(cpuUsage.user / 1000), system: Math.round(cpuUsage.system / 1000) }, connections: { websocket: wss.clients.size, http: server._connections || 0 } }; this.records.push(record); if (this.records.length > MAX_MEMORY_RECORDS) { this.records = this.records.slice(-MAX_MEMORY_RECORDS); } if (record.memory.heapUsed > 450) { if (!this.highMemoryAlerted) { memLogger.warn('ALERT', 'High memory usage detected', { heapUsed: `${record.memory.heapUsed} MB`, threshold: '450 MB' }); this.highMemoryAlerted = true; fileStorage.saveAnalytics('memory_alert', { memory: record.memory, uptime: record.uptime, connections: record.connections }); } } else if (record.memory.heapUsed < 400) { this.highMemoryAlerted = false; } if (this.records.length % 20 === 0) { this.saveToStorage(); } return record; } saveToStorage() { try { fileStorage.saveAnalytics('memory_snapshot', { records: this.records.slice(-100), metadata: this.metadata, stats: this.getStats() }); } catch (error) { memLogger.error('SAVE', 'Failed to save memory snapshot', error); } } getStats() { const recent = this.records.slice(-100); if (recent.length === 0) { return { current: this.record(), average: null, peak: null, total: 0 }; } const avgMemory = { rss: Math.round(recent.reduce((sum, r) => sum + r.memory.rss, 0) / recent.length), heapUsed: Math.round(recent.reduce((sum, r) => sum + r.memory.heapUsed, 0) / recent.length) }; const peakMemory = { rss: Math.max(...recent.map(r => r.memory.rss)), heapUsed: Math.max(...recent.map(r => r.memory.heapUsed)), timestamp: recent.find(r => r.memory.heapUsed === Math.max(...recent.map(x => x.memory.heapUsed)))?.timestamp }; return { current: recent[recent.length - 1], average: avgMemory, peak: peakMemory, total: this.records.length, retention: `${Math.round((Date.now() - new Date(this.metadata.createdAt).getTime()) / (1000 * 60 * 60))} hours` }; } exportCSV() { const headers = 'timestamp,uptime,rss,heapTotal,heapUsed,external,websocket_connections\n'; const rows = this.records.map(r => `${r.timestamp},${r.uptime},${r.memory.rss},${r.memory.heapTotal},${r.memory.heapUsed},${r.memory.external},${r.connections?.websocket || 0}` ).join('\n'); return headers + rows; } } const memoryMonitor = new MemoryMonitor(); // ============================================================================ // HELPER FUNCTIONS // ============================================================================ const getClientIP = (req) => { if (IS_RAILWAY) { return req.headers['x-forwarded-for']?.split(',')[0].trim() || req.headers['x-real-ip'] || req.headers['cf-connecting-ip'] || req.connection?.remoteAddress?.replace('::ffff:', '') || 'unknown'; } const forwarded = req.headers['x-forwarded-for']; const realIp = req.headers['x-real-ip']; const cfIp = req.headers['cf-connecting-ip']; if (process.env.BEHIND_PROXY === 'true') { if (cfIp) return cfIp; if (realIp) return realIp; if (forwarded) return forwarded.split(',')[0].trim(); } return req.connection?.remoteAddress?.replace('::ffff:', '') || req.socket?.remoteAddress?.replace('::ffff:', '') || 'unknown'; }; const generateSessionId = () => { return `sess_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`; }; app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); // Detailed health (restricted) app.get('/health/detailed', (req, res) => { // Only allow from localhost or specific IPs const ip = getClientIP(req); if (ip !== '127.0.0.1' && ip !== '::1' && !ip.startsWith('10.') && !ip.startsWith('192.168.')) { security.recordSuspiciousActivity(ip, 'Unauthorized health check access', 2); return res.status(403).json({ error: 'Access denied' }); } const memStats = memoryMonitor.getStats(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: memStats.current?.memory || process.memoryUsage(), connections: { websocket: wss.clients.size, http: server._connections || 0 }, environment: { node: process.version, platform: IS_RAILWAY ? 'Railway' : 'Local', env: process.env.NODE_ENV || 'production' } }); }); // Welcome endpoint app.get('/api/welcome', (req, res) => { logger.info(req.sessionId, 'Welcome endpoint accessed', { method: req.method, path: req.path, ip: req.clientIP, userAgent: req.headers['user-agent'] }); res.json({ message: 'Welcome to the API service!', timestamp: new Date().toISOString(), request: { method: req.method, path: req.path } }); }); // Memory stats - authenticated app.get('/api/memory/stats', (req, res) => { const ip = getClientIP(req); // Basic authentication check (you should implement proper auth) const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${config.adminToken}`) { security.recordFailedAttempt(ip, 'Unauthorized memory stats access'); return res.status(401).json({ error: 'Unauthorized' }); } try { const stats = memoryMonitor.getStats(); res.json({ success: true, data: stats, timestamp: new Date().toISOString() }); } catch (error) { memLogger.error('STATS', 'Failed to get stats', error); res.status(500).json({ error: 'Failed to retrieve memory stats' }); } }); // Export memory stats - authenticated app.get('/api/memory/export', (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${config.adminToken}`) { security.recordFailedAttempt(ip, 'Unauthorized memory export'); return res.status(401).json({ error: 'Unauthorized' }); } try { const csv = memoryMonitor.exportCSV(); res.setHeader('Content-Type', 'text/csv'); res.setHeader('Content-Disposition', `attachment; filename="memory-stats-${Date.now()}.csv"`); res.send(csv); logger.success(req.sessionId, 'Memory stats exported'); } catch (error) { memLogger.error('EXPORT', 'Failed to export', error); res.status(500).json({ error: 'Failed to export memory stats' }); } }); app.get('/api/search-proxy', async (req, res) => { const startTime = Date.now(); const { q } = req.query; const ip = req.clientIP; try { if (!validators.searchQuery(q)) { security.recordSuspiciousActivity(ip, 'Invalid search query format', 2); return res.status(400).json({ error: 'Invalid search query', suggestions: [], count: 0 }); } const sanitizedQuery = security.sanitize(q); const externalAPI = `https://lok-tantra-10qg0fa2f-rex1671s-projects.vercel.app/api/search?q=${encodeURIComponent(sanitizedQuery)}`; logger.info(req.sessionId, `Search: "${sanitizedQuery}"`, { ip }); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const response = await fetch(externalAPI, { method: 'GET', headers: { 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0 (compatible; LokTantraBot/1.0)' }, signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { throw new Error(`API returned ${response.status}`); } const data = await response.json(); const duration = Date.now() - startTime; // ✅ GET BASE URL FOR PROXY let host = req.get('host'); if (host.includes('0.0.0.0')) { host = host.replace('0.0.0.0', 'localhost'); } // In development, ignore BASE_URL env var to ensure we use localhost const baseUrl = IS_DEVELOPMENT ? `${req.protocol}://${host}` : (process.env.BASE_URL || `${req.protocol}://${host}`); // ✅ PROCESS EACH SUGGESTION - PROXY IMAGES & REMOVE SENSITIVE DATA const processedSuggestions = (data.suggestions || []).map(suggestion => { const processed = { ...suggestion }; // Extract meow and bhaw from link before removing it let meow = ''; let bhaw = ''; if (processed.link) { try { const url = new URL(processed.link); const params = new URLSearchParams(url.search); meow = params.get('candidate_id') || ''; const pathParts = url.pathname.split('/'); const stateIndex = pathParts.findIndex(part => part && part !== ''); if (stateIndex !== -1) { bhaw = pathParts[stateIndex]; } } catch (error) { logger.warn(req.sessionId, 'Failed to parse link URL', { link: processed.link }); } } // Add extracted values if (meow) processed.meow = meow; if (bhaw) processed.bhaw = bhaw; // ✅ PROXY THE IMAGE if (processed.image) { const proxyUrl = imageProxy.createProxyUrl(processed.image, baseUrl); if (proxyUrl) { processed.imageUrl = proxyUrl; processed._imageProxied = true; logger.info(req.sessionId, 'Image proxied for search result', { name: processed.name, original: processed.image.substring(0, 50) + '...', proxied: proxyUrl }); } else { // Fallback to original if proxy fails processed.imageUrl = processed.image; processed._imageProxied = false; } // ✅ REMOVE ORIGINAL IMAGE URL delete processed.image; } // ✅ REMOVE SENSITIVE LINK delete processed.link; return processed; }); logger.success(req.sessionId, `Search completed: ${processedSuggestions.length} results`, { duration: `${duration}ms`, query: sanitizedQuery, proxiedImages: processedSuggestions.filter(s => s._imageProxied).length }); fileStorage.saveAnalytics('search_query', { query: sanitizedQuery, results: processedSuggestions.length, duration, ip, timestamp: new Date().toISOString() }); res.json({ suggestions: processedSuggestions, count: processedSuggestions.length, responseTime: duration, timestamp: new Date().toISOString() }); } catch (error) { const duration = Date.now() - startTime; if (error.name === 'AbortError' || error.name === 'TimeoutError') { logger.warn(req.sessionId, 'Search timeout', { duration, query: q }); } else { logger.error(req.sessionId, 'Search failed', error); } res.status(200).json({ suggestions: [], count: 0, error: 'Search temporarily unavailable', fallback: true }); } }); // Analytics - authenticated app.get('/api/analytics', async (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { security.recordFailedAttempt(ip, 'Unauthorized analytics access'); return res.status(401).json({ error: 'Unauthorized' }); } try { const { days = 7 } = req.query; if (!validators.numericParam(days, 1, 365)) { return res.status(400).json({ error: 'Invalid days parameter' }); } const summary = fileStorage.getAnalyticsSummary(parseInt(days)); res.json({ period: `Last ${days} days`, ...summary, storage: fileStorage.getStats(), timestamp: new Date().toISOString() }); } catch (error) { logger.error(req.sessionId, 'Analytics fetch failed', error); res.status(500).json({ error: 'Failed to fetch analytics' }); } }); // Security stats - authenticated app.get('/api/security/stats', (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { security.recordFailedAttempt(ip, 'Unauthorized security stats access'); return res.status(401).json({ error: 'Unauthorized' }); } res.json({ success: true, data: security.getStats(), timestamp: new Date().toISOString() }); }); // Manual IP block - authenticated app.post('/api/security/block', (req, res) => { const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { return res.status(401).json({ error: 'Unauthorized' }); } const { ip, reason, duration } = req.body; if (!ip || typeof ip !== 'string') { return res.status(400).json({ error: 'Invalid IP address' }); } security.blockIP(ip, reason || 'Manual block', duration); res.json({ success: true, message: `IP ${ip} blocked`, timestamp: new Date().toISOString() }); }); // Storage search - validated app.get('/api/storage/search', (req, res) => { const { q } = req.query; if (!q || typeof q !== 'string' || q.length < 3 || q.length > 200) { return res.status(400).json({ error: 'Query must be between 3-200 characters' }); } const sanitizedQuery = security.sanitize(q); const results = fileStorage.searchCandidates(sanitizedQuery); logger.info(req.sessionId, `Storage search: "${sanitizedQuery}"`, { results: results.length }); res.json({ query: sanitizedQuery, results, count: results.length, timestamp: new Date().toISOString() }); }); // Backup - authenticated app.post('/api/storage/backup', async (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { security.recordFailedAttempt(ip, 'Unauthorized backup access'); return res.status(401).json({ error: 'Unauthorized' }); } try { logger.info(req.sessionId, 'Backup initiated'); const result = await fileStorage.createBackup(); logger.success(req.sessionId, 'Backup completed', { filepath: result.filepath }); res.json(result); } catch (error) { logger.error(req.sessionId, 'Backup failed', error); res.status(500).json({ error: error.message }); } }); // Storage stats - authenticated app.get('/api/storage/stats', (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { security.recordFailedAttempt(ip, 'Unauthorized storage stats access'); return res.status(401).json({ error: 'Unauthorized' }); } try { const stats = fileStorage.getStats(); res.json({ success: true, data: stats, timestamp: new Date().toISOString() }); } catch (error) { logger.error(req.sessionId, 'Storage stats failed', error); res.status(500).json({ error: 'Failed to retrieve storage stats' }); } }); // Manual cleanup - authenticated app.post('/api/storage/cleanup', async (req, res) => { const ip = getClientIP(req); const authToken = req.headers['authorization']; if (!authToken || authToken !== `Bearer ${process.env.ADMIN_TOKEN}`) { security.recordFailedAttempt(ip, 'Unauthorized cleanup access'); return res.status(401).json({ error: 'Unauthorized' }); } try { logger.info(req.sessionId, 'Manual cleanup initiated', { ip }); const logResult = await cleanupScheduler.runLogCleanup(); const storageResult = await cleanupScheduler.runStorageCleanup(); res.json({ success: true, message: 'Cleanup completed', results: { logs: logResult, storage: storageResult }, timestamp: new Date().toISOString() }); } catch (error) { logger.error(req.sessionId, 'Manual cleanup failed', error); res.status(500).json({ error: 'Cleanup failed' }); } }); // Apply rate limiters app.use('/api/', (req, res, next) => { apiLimiter(req, res, (err) => { if (err && err.statusCode === 429) { logger.warn(req.sessionId, 'API rate limit exceeded', { ip: req.clientIP, path: req.path }); security.recordSuspiciousActivity(req.clientIP, 'API rate limit exceeded', 2); fileStorage.saveAnalytics('rate_limit', { type: 'api', ip: req.clientIP, path: req.path, timestamp: new Date().toISOString() }); } next(err); }); }); app.use('/member/', (req, res, next) => { memberLimiter(req, res, (err) => { if (err && err.statusCode === 429) { logger.warn(req.sessionId, 'Member rate limit exceeded', { ip: req.clientIP, path: req.path }); security.recordSuspiciousActivity(req.clientIP, 'Member rate limit exceeded', 2); } next(err); }); }); // Static files with security app.use(express.static('public', { dotfiles: 'deny', index: false, setHeaders: (res, path) => { // Prevent caching of sensitive files if (path.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } } })); app.use('/api', apiRoutes); app.use('/member', memberRoutes); // Serve index with environment injection app.get('/', (req, res) => { try { const htmlPath = path.join(__dirname, 'public', 'index.html'); let html = fs.readFileSync(htmlPath, 'utf8'); html = html.replace('', ``); res.send(html); } catch (error) { logger.error(req.sessionId, 'Failed to serve index', error); res.status(500).send('Server error'); } }); // ============================================================================ // WEBSOCKET HANDLING - ENHANCED SECURITY // ============================================================================ const wsConnections = new Map(); const CLIENT_TIMEOUT = 5 * 60 * 1000; const MAX_MESSAGE_SIZE = 10 * 1024; // 10KB max message size wss.on('connection', (ws, req) => { const clientIp = getClientIP(req); const sessionId = 'ws_' + crypto.randomBytes(8).toString('hex'); const userAgent = req.headers['user-agent'] || 'unknown'; // Check if IP is blocked if (security.isBlocked(clientIp)) { wsLogger.warn(sessionId, 'Blocked IP attempted WebSocket connection', { ip: clientIp }); ws.close(1008, 'Access denied'); return; } // Limit connections per IP const currentConnections = wsConnectionLimiter.get(clientIp) || 0; if (currentConnections >= security.THRESHOLDS.MAX_WS_CONNECTIONS_PER_IP) { wsLogger.warn(sessionId, 'Too many WebSocket connections from IP', { ip: clientIp, current: currentConnections, max: security.THRESHOLDS.MAX_WS_CONNECTIONS_PER_IP }); security.recordSuspiciousActivity(clientIp, 'WebSocket connection limit exceeded', 3); ws.close(1008, 'Too many connections'); return; } wsConnectionLimiter.set(clientIp, currentConnections + 1); ws.sessionId = sessionId; ws.clientIp = clientIp; ws.lastActivity = Date.now(); ws.tempIds = new Set(); ws.connectionTime = new Date(); ws.messageCount = 0; wsConnections.set(sessionId, { ip: clientIp, userAgent, connectedAt: new Date(), subscriptions: [] }); wsLogger.success(sessionId, 'Client connected', { ip: clientIp, total: wss.clients.size }); fileStorage.saveAnalytics('websocket_connection', { sessionId, ip: clientIp, userAgent, timestamp: new Date().toISOString() }); ws.on('message', (message) => { try { // Check message size if (message.length > MAX_MESSAGE_SIZE) { wsLogger.warn(sessionId, 'Message too large', { size: message.length, max: MAX_MESSAGE_SIZE }); security.recordSuspiciousActivity(clientIp, 'WebSocket message too large', 3); ws.close(1009, 'Message too large'); return; } // Rate limit messages ws.messageCount++; if (ws.messageCount > 100) { // Max 100 messages per connection wsLogger.warn(sessionId, 'Too many messages', { count: ws.messageCount }); security.recordSuspiciousActivity(clientIp, 'WebSocket message spam', 4); ws.close(1008, 'Too many messages'); return; } const data = JSON.parse(message); // Validate message structure if (!data.type || typeof data.type !== 'string') { wsLogger.warn(sessionId, 'Invalid message format'); return; } // Scan for threats const threats = security.scanForThreats(data); if (threats.length > 0) { wsLogger.warn(sessionId, 'Malicious WebSocket message', { threats }); security.recordSuspiciousActivity(clientIp, 'Malicious WebSocket message', 5); ws.close(1008, 'Invalid message'); return; } if (data.type === 'subscribe' && data.tempId) { // Limit subscriptions if (ws.tempIds.size >= 50) { wsLogger.warn(sessionId, 'Too many subscriptions'); return; } ws.tempIds.add(data.tempId); ws.lastActivity = Date.now(); const connInfo = wsConnections.get(sessionId); if (connInfo) { connInfo.subscriptions.push(data.tempId); } wsLogger.info(sessionId, `Subscribed to ${data.tempId}`); } } catch (err) { wsLogger.error(sessionId, 'Message processing error', err); security.recordSuspiciousActivity(clientIp, 'Invalid WebSocket message', 1); } }); ws.on('close', (code, reason) => { const duration = Date.now() - ws.connectionTime.getTime(); // Decrement connection counter const currentCount = wsConnectionLimiter.get(clientIp) || 1; wsConnectionLimiter.set(clientIp, currentCount - 1); if (currentCount <= 1) { wsConnectionLimiter.delete(clientIp); } wsConnections.delete(sessionId); wsLogger.info(sessionId, 'Client disconnected', { duration: `${Math.round(duration / 1000)}s`, code, remaining: wss.clients.size }); fileStorage.saveAnalytics('websocket_disconnection', { sessionId, duration, code, messageCount: ws.messageCount, timestamp: new Date().toISOString() }); }); ws.on('error', (error) => { wsLogger.error(sessionId, 'WebSocket error', error); }); // Ping interval with cleanup const pingInterval = setInterval(() => { if (ws.readyState === ws.OPEN) { ws.ping(); } else { clearInterval(pingInterval); } }, 30000); ws.on('pong', () => { ws.lastActivity = Date.now(); }); }); // Cleanup idle connections setInterval(() => { const now = Date.now(); let cleaned = 0; wss.clients.forEach((ws) => { if (now - ws.lastActivity > CLIENT_TIMEOUT) { ws.close(1000, 'Idle timeout'); cleaned++; } }); if (cleaned > 0) { wsLogger.info('CLEANUP', `Cleaned ${cleaned} inactive connections`, { active: wss.clients.size }); } }, 60000); // ============================================================================ // ERROR HANDLING - SECURE // ============================================================================ app.use((err, req, res, next) => { // Don't log expected errors if (err.statusCode && err.statusCode < 500) { logger.warn(req.sessionId, 'Client error', { error: err.message, status: err.statusCode }); } else { logger.error(req.sessionId, 'Server error', err); } const statusCode = err.statusCode || err.status || 500; // Never expose error details in production const errorResponse = { error: statusCode === 500 ? 'Internal server error' : err.message, requestId: req.sessionId }; // Add stack trace only in development if (IS_DEVELOPMENT && err.stack) { errorResponse.stack = err.stack; } res.status(statusCode).json(errorResponse); }); // 404 handler app.use((req, res) => { const ip = getClientIP(req); logger.warn(req.sessionId, `404 Not Found: ${req.path}`, { method: req.method, ip }); // Track excessive 404s as suspicious const key = `404_${ip}`; const count = (security.suspiciousActivity.get(key)?.count || 0) + 1; if (count > 20) { security.recordSuspiciousActivity(ip, 'Excessive 404 requests', 2); } res.status(404).json({ error: 'Not found', path: req.path }); }); // ============================================================================ // GRACEFUL SHUTDOWN // ============================================================================ const shutdown = async (signal) => { logger.warn('SHUTDOWN', `Shutting down gracefully... (${signal})`); // Save final memory stats memoryMonitor.saveToStorage(); memLogger.success('SHUTDOWN', 'Memory stats saved'); // Create backup try { await fileStorage.createBackup(); logger.success('SHUTDOWN', 'Backup created'); } catch (error) { logger.error('SHUTDOWN', 'Backup failed', error); } // Close WebSocket connections let wsCount = 0; wss.clients.forEach(client => { client.close(1001, 'Server shutting down'); wsCount++; }); logger.info('SHUTDOWN', `Closed ${wsCount} WebSocket connections`); // Stop cleanup scheduler cleanupScheduler.stop(); // Close server server.close(() => { logger.success('SHUTDOWN', 'Server closed successfully'); process.exit(0); }); // Force shutdown after timeout setTimeout(() => { logger.error('SHUTDOWN', 'Forced shutdown after timeout'); process.exit(1); }, 10000); }; process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); process.on('uncaughtException', (error) => { logger.error('FATAL', 'Uncaught Exception', error); console.error(error.stack); shutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { logger.error('FATAL', 'Unhandled Rejection', { reason, promise }); console.error(reason); }); // ============================================================================ // HEALTH MONITORING // ============================================================================ const monitoringInterval = IS_RAILWAY ? 15 * 60 * 1000 : 5 * 60 * 1000; setInterval(() => { const record = memoryMonitor.record(); if (record.memory.heapUsed > 400 || IS_DEVELOPMENT) { memLogger.info('MONITOR', 'Health check', { memory: `${record.memory.heapUsed}MB / ${record.memory.heapTotal}MB`, connections: record.connections, uptime: `${Math.round(record.uptime / 60)}min`, security: { blockedIPs: security.blockedIPs.size, suspiciousIPs: security.suspiciousActivity.size } }); } }, monitoringInterval); // ============================================================================ // START SERVER // ============================================================================ const PORT = process.env.PORT || config.server.port || 3000; const HOST = '0.0.0.0'; server.listen(PORT, HOST, () => { logger.success('STARTUP', `🔒 Secured server running at http://${HOST}:${PORT}`, { environment: process.env.NODE_ENV || 'production', platform: IS_RAILWAY ? 'Railway' : 'Local', logToFile: process.env.LOG_TO_FILE === 'true', trustProxy: app.get('trust proxy'), security: 'ENABLED' }); if (IS_RAILWAY) { logger.info('STARTUP', 'Railway Configuration', { project: process.env.RAILWAY_PROJECT_ID, environment: process.env.RAILWAY_ENVIRONMENT_ID }); } logger.info('STARTUP', 'Security Configuration', { ipBlocking: 'enabled', rateLimit: 'enabled', inputValidation: 'enabled', threatDetection: 'enabled', wsConnectionLimit: security.THRESHOLDS.MAX_WS_CONNECTIONS_PER_IP }); logger.info('STARTUP', 'Cache Configuration', { prsTTL: `${config.cache?.ttl?.prs || 3600}s`, candidateTTL: `${config.cache?.ttl?.candidate || 3600}s` }); logger.info('STARTUP', 'Monitoring', { interval: `${monitoringInterval / 1000 / 60}min`, fileStorage: 'enabled' }); cleanupScheduler.start(); // Image proxy cleanup const imageCleanupInterval = 2 * 60 * 60 * 1000; setInterval(() => { imageProxy.cleanup(); logger.info('IMAGE-CLEANUP', 'Old image mappings cleaned'); }, imageCleanupInterval); logger.info('STARTUP', 'Image proxy cleanup scheduled', { interval: '2 hours' }); logger.success('STARTUP', '✅ All systems operational'); }); export { app, server, wss };