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 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}`); } } 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, 100000, 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 { const apiKey = req.headers["x-api-key"]; // Validate API key header if (!apiKey || !isValidApiKey(apiKey)) { return res .status(403) .json({ error: "Invalid or missing X-API-Key header" }); } // 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: "OK" }); } 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 { const apiKey = req.headers["x-api-key"]; // Validate API key header if (!apiKey || !isValidApiKey(apiKey)) { return res .status(403) .json({ error: "Invalid or missing X-API-Key header" }); } 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 { const apiKey = req.headers["x-api-key"]; // Validate API key header if (!apiKey || !isValidApiKey(apiKey)) { return res .status(403) .json({ error: "Invalid or missing X-API-Key header" }); } 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`, ); console.log( " Security: AES-256-GCM encryption with PBKDF2 (100k iterations)", ); // 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}`); }); } module.exports = { createApp };