const express = require('express'); const path = require('path'); const crypto = require('crypto'); const os = require('os'); const { v4: uuidv4 } = require('uuid'); const { parseArgs } = require('util'); // 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(devMode = false) { 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'))); // Dev mode request logging middleware if (devMode) { app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`๐Ÿ“จ [${timestamp}] ${req.method} ${req.path}`); if (req.method !== 'GET' && Object.keys(req.body).length > 0) { const bodySize = Buffer.byteLength(JSON.stringify(req.body), 'utf8'); console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`); } const originalJson = res.json; res.json = function(data) { console.log(` โœ“ Response: ${res.statusCode}`); return originalJson.call(this, data); }; next(); }); } // 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.' }); } } }); // Status endpoint (no auth required) - detailed information app.get('/api/v1/status', (req, res) => { cleanupExpiredSessions(); // Cleanup on status 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() }); }); // Health endpoint (no auth required) - simple OK response app.get('/api/v1/health', (req, res) => { res.type('text/plain').send('OK'); }); // 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) { const { values } = parseArgs({ options: { 'listen-addr': { type: 'string', short: 'h', default: process.env.LISTEN_ADDR || '127.0.0.1' }, 'port': { type: 'string', short: 'p', default: process.env.LISTEN_PORT || '3000' }, 'dev': { type: 'boolean', default: process.env.DEV_MODE === 'true' || false } } }); const DEV_MODE = values.dev; const app = createApp(DEV_MODE); const PORT = parseInt(values.port); const HOST = values['listen-addr']; app.listen(PORT, HOST, () => { console.log(`JMESPath Playground Server running`); if (DEV_MODE) { console.log(` ๐Ÿ”ง Development Mode Enabled`); } // Show actual accessible URLs if (HOST === '0.0.0.0') { console.log(` Listening on all interfaces:`); const interfaces = os.networkInterfaces(); for (const [name, addrs] of Object.entries(interfaces)) { for (const addr of addrs) { if (addr.family === 'IPv4' && !addr.internal) { console.log(` http://${addr.address}:${PORT}`); } } } // Also show localhost for local access console.log(` http://127.0.0.1:${PORT}`); } else { console.log(` 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`); // Show base API URL let apiBaseUrl; if (HOST === '0.0.0.0') { const interfaces = os.networkInterfaces(); let firstIP = '127.0.0.1'; outer: for (const addrs of Object.values(interfaces)) { for (const addr of addrs) { if (addr.family === 'IPv4' && !addr.internal) { firstIP = addr.address; break outer; } } } apiBaseUrl = `http://${firstIP}:${PORT}/api/v1`; } else { apiBaseUrl = `http://${HOST}:${PORT}/api/v1`; } console.log(`API Base URL: ${apiBaseUrl}`); console.log(`Security: AES-256-GCM encryption with PBKDF2 key derivation`); }); } module.exports = { createApp };