const express = require('express'); const path = require('path'); const crypto = require('crypto'); const { v4: uuidv4 } = require('uuid'); // Environment configuration const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100; const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; // 1MB const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour // Utility functions for encryption function generateSalt() { return crypto.randomBytes(16); } function isLocalhostRequest(req) { // Get client IP with fallback options const forwarded = req.get('X-Forwarded-For'); const ip = forwarded ? forwarded.split(',')[0].trim() : req.ip || req.connection.remoteAddress || req.socket.remoteAddress || '127.0.0.1'; const host = req.get('host') || ''; // Check for localhost IP addresses (IPv4 and IPv6) const localhostIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost']; const isLocalIP = localhostIPs.includes(ip) || ip.startsWith('127.') || ip === '::1'; // Check for localhost hostnames const isLocalHost = host.startsWith('localhost:') || host.startsWith('127.0.0.1:') || host === 'localhost' || host === '127.0.0.1'; return isLocalIP || isLocalHost; } function encrypt(data, key) { try { const algorithm = 'aes-256-gcm'; const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, key, iv); cipher.setAAD(Buffer.from('session-data')); let encrypted = cipher.update(JSON.stringify(data), 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); const authTag = cipher.getAuthTag(); return { iv: iv.toString('hex'), data: encrypted.toString('hex'), tag: authTag.toString('hex') }; } catch (error) { console.error('โš ๏ธ Encryption exception:', { message: error.message, algorithm: 'aes-256-gcm', keyLength: key ? key.length : 'undefined', timestamp: new Date().toISOString() }); throw new Error(`Encryption failed: ${error.message}`); } } function decrypt(encryptedObj, key) { try { const algorithm = 'aes-256-gcm'; const iv = Buffer.from(encryptedObj.iv, 'hex'); const decipher = crypto.createDecipheriv(algorithm, key, iv); decipher.setAAD(Buffer.from('session-data')); decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex')); let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } catch (error) { console.error('โš ๏ธ Decryption exception:', { message: error.message, algorithm: 'aes-256-gcm', keyLength: key ? key.length : 'undefined', hasIV: !!encryptedObj.iv, hasTag: !!encryptedObj.tag, hasData: !!encryptedObj.data, timestamp: new Date().toISOString() }); throw new Error(`Decryption failed: ${error.message}`); } } // For localhost requests, use a consistent API key so sessions persist const LOCALHOST_API_KEY = 'localhost0123456789abcdef0123456789'; function isValidApiKey(apiKey) { return typeof apiKey === 'string' && /^[0-9a-f]{32}$/i.test(apiKey); } function getSessionId(apiKey) { return crypto.createHash('sha256').update(apiKey).digest('hex'); } function generateSalt() { return crypto.randomBytes(32); } function deriveKey(apiKey, salt) { return crypto.pbkdf2Sync(apiKey, salt, 10000, 32, 'sha256'); } // Create Express app function createApp() { const app = express(); // Trust proxy to get real client IP (needed for localhost detection) app.set('trust proxy', true); // Middleware app.use(express.json({ limit: MAX_SAMPLE_SIZE })); app.use(express.static(path.join(__dirname, 'build'))); // Session storage const sessions = new Map(); // Cleanup expired sessions function cleanupExpiredSessions() { const now = Date.now(); for (const [sessionId, session] of sessions.entries()) { if (now - session.createdAt > MAX_SESSION_TTL) { sessions.delete(sessionId); console.log(`๐Ÿงน Cleaned up expired session: ${sessionId.substring(0, 8)}...`); } } } // Run cleanup every 5 minutes setInterval(cleanupExpiredSessions, 5 * 60 * 1000); // API endpoints app.post('/api/v1/upload', (req, res) => { try { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); } } else { // For localhost requests, use consistent API key for session persistence if (!apiKey || !isValidApiKey(apiKey)) { apiKey = LOCALHOST_API_KEY; } } // Cleanup expired sessions before checking limits cleanupExpiredSessions(); // Check session limits if (sessions.size >= MAX_SESSIONS) { return res.status(429).json({ error: 'Maximum number of sessions reached. Please try again later.', maxSessions: MAX_SESSIONS, currentSessions: sessions.size }); } const uploadedData = req.body; // Validate that it's valid JSON if (!uploadedData || typeof uploadedData !== 'object') { return res.status(400).json({ error: 'Invalid JSON data' }); } // Check data size const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8'); if (dataSize > MAX_SAMPLE_SIZE) { return res.status(413).json({ error: 'Sample data too large', maxSize: MAX_SAMPLE_SIZE, receivedSize: dataSize }); } const sessionId = getSessionId(apiKey); const salt = generateSalt(); const key = deriveKey(apiKey, salt); const stateGuid = uuidv4(); // Encrypt and store session data const encryptedData = encrypt(uploadedData, key); sessions.set(sessionId, { salt: salt.toString('hex'), encryptedData, state: stateGuid, createdAt: Date.now(), accessed: false }); console.log(`๐Ÿ“ Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`); res.json({ message: 'Sample data uploaded successfully', state: stateGuid, sessionId: sessionId.substring(0, 8) + '...' }); } catch (error) { console.error('โš ๏ธ Upload endpoint exception occurred:', { message: error.message, stack: error.stack, sessionCount: sessions.size, timestamp: new Date().toISOString() }); // Provide more specific error messages based on error type if (error.name === 'SyntaxError') { return res.status(400).json({ error: 'Invalid JSON data format', details: 'The uploaded data could not be parsed as valid JSON' }); } else if (error.message.includes('encrypt')) { return res.status(500).json({ error: 'Encryption failed', details: 'Failed to encrypt session data. Please try again with a new API key.' }); } else if (error.message.includes('PBKDF2')) { return res.status(500).json({ error: 'Key derivation failed', details: 'Failed to derive encryption key from API key' }); } else { return res.status(500).json({ error: 'Upload processing failed', details: 'An unexpected error occurred while processing your upload. Please try again.' }); } } }); app.get('/api/v1/sample', (req, res) => { try { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); } } else { // For localhost requests, use consistent API key for session persistence if (!apiKey || !isValidApiKey(apiKey)) { apiKey = LOCALHOST_API_KEY; } } const sessionId = getSessionId(apiKey); const session = sessions.get(sessionId); if (!session) { return res.json(null); } // Decrypt data const salt = Buffer.from(session.salt, 'hex'); const key = deriveKey(apiKey, salt); const decryptedData = decrypt(session.encryptedData, key); // Remove session after first access (one-time use) sessions.delete(sessionId); console.log(`๐Ÿ“ค Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`); res.json(decryptedData); } catch (error) { console.error('โš ๏ธ Sample retrieval exception occurred:', { message: error.message, stack: error.stack, sessionCount: sessions.size, timestamp: new Date().toISOString() }); // Provide more specific error messages based on error type if (error.message.includes('decrypt')) { return res.status(500).json({ error: 'Decryption failed', details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.' }); } else if (error.message.includes('JSON')) { return res.status(500).json({ error: 'Data corruption detected', details: 'The stored session data appears to be corrupted and cannot be parsed.' }); } else if (error.name === 'TypeError') { return res.status(500).json({ error: 'Session data format error', details: 'The session data format is invalid or corrupted.' }); } else { return res.status(500).json({ error: 'Sample retrieval failed', details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.' }); } } }); app.get('/api/v1/state', (req, res) => { try { // Check if request is from localhost - if so, skip API key validation const isFromLocalhost = isLocalhostRequest(req); let apiKey = req.headers['x-api-key']; if (!isFromLocalhost) { // Validate API key header for remote clients if (!apiKey || !isValidApiKey(apiKey)) { return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); } } else { // For localhost requests, use consistent API key for session persistence if (!apiKey || !isValidApiKey(apiKey)) { apiKey = LOCALHOST_API_KEY; } } const sessionId = getSessionId(apiKey); const session = sessions.get(sessionId); if (!session) { // Return null state when no session exists return res.json({ state: null }); } res.json({ state: session.state }); } catch (error) { console.error('โš ๏ธ State retrieval exception occurred:', { message: error.message, stack: error.stack, sessionCount: sessions.size, timestamp: new Date().toISOString() }); // Provide more specific error messages if (error.message.includes('API key')) { return res.status(403).json({ error: 'API key processing failed', details: 'Failed to process the provided API key' }); } else { return res.status(500).json({ error: 'State retrieval failed', details: 'An unexpected error occurred while retrieving session state. Please try again.' }); } } }); // Health endpoint (no auth required) app.get('/api/v1/health', (req, res) => { cleanupExpiredSessions(); // Cleanup on health check res.json({ status: 'healthy', sessions: { current: sessions.size, max: MAX_SESSIONS, available: MAX_SESSIONS - sessions.size }, limits: { maxSessions: MAX_SESSIONS, maxSampleSize: MAX_SAMPLE_SIZE, maxSessionTTL: MAX_SESSION_TTL }, uptime: process.uptime() }); }); // Serve React app for all other routes app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'build', 'index.html')); }); return app; } // Start server if this file is run directly if (require.main === module) { // Parse command line arguments const args = process.argv.slice(2); let listenAddr = process.env.LISTEN_ADDR || '127.0.0.1'; let listenPort = process.env.LISTEN_PORT || 3000; for (let i = 0; i < args.length; i++) { if (args[i] === '-h' || args[i] === '--listen-addr') { listenAddr = args[i + 1]; i++; } else if (args[i] === '-p' || args[i] === '--port') { listenPort = args[i + 1]; i++; } } const app = createApp(); const PORT = parseInt(listenPort); const HOST = listenAddr; app.listen(PORT, HOST, () => { console.log(`๐Ÿš€ JMESPath Playground Server running on http://${HOST}:${PORT}`); console.log(`๐Ÿ“Š Configuration:`); console.log(` Max Sessions: ${MAX_SESSIONS}`); console.log(` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`); console.log(` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`); console.log(`๐Ÿ”— API endpoints:`); console.log(` POST http://${HOST}:${PORT}/api/v1/upload (requires X-API-Key)`); console.log(` GET http://${HOST}:${PORT}/api/v1/sample (requires X-API-Key)`); console.log(` GET http://${HOST}:${PORT}/api/v1/state (requires X-API-Key)`); console.log(` GET http://${HOST}:${PORT}/api/v1/health (public)`); console.log(`๐Ÿ” Security: AES-256-GCM encryption with PBKDF2 key derivation`); }); } module.exports = { createApp };