From 4cb7eec65537236fc3836363942cf8a243d7755d Mon Sep 17 00:00:00 2001 From: Kajal Thakur Date: Tue, 21 Jan 2025 09:42:28 +0000 Subject: [PATCH] code update recorded at: 21/01/25 09:42:28 --- handler.js | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 19 ++++++ 2 files changed, 199 insertions(+) create mode 100644 handler.js create mode 100644 package.json diff --git a/handler.js b/handler.js new file mode 100644 index 0000000..8bc7f6d --- /dev/null +++ b/handler.js @@ -0,0 +1,180 @@ +import Redis from 'ioredis'; +import { createTunnel } from 'tunnel-ssh'; + +// Maximum allowed connections in the connection pool +const MAX_CONNECTIONS = parseInt(process.env.MAX_CONNECTIONS || '10'); +// Connection pool to manage Redis connections +const connectionPool = new Map(); +// Timeout for Redis connections, configurable via environment variables +const CONNECTION_TIMEOUT = parseInt(process.env.CONNECTION_TIMEOUT || '10000'); + +/** + * Validates the request body for required fields and structure. + * Ensures all necessary fields for Redis and SSH configurations are provided. + * @param {Object} body - Request body containing Redis commands and secretData. + * @throws {Error} If validation fails. + */ +const validateRequestBody = ({ command, key, secretData }) => { + const errors = []; + + // Ensure the Redis command is provided + if (!command) errors.push('Redis command is required.'); + if (!key) errors.push('Key is required for selected command.'); + + // Validate Redis configuration + const { host, port, database_number, password } = secretData || {}; + + if (!host) errors.push('db_config.host is required.'); + if (!port) errors.push('db_config.port is required.'); + if (database_number === "") errors.push('db_config.database_number is required.'); + + // Throw error if any validation fails + if (errors.length) { + var msg = `Validation errors: ${errors.join(' ')}` + console.log(msg) + throw new Error(msg); + } +}; + +/** + * Creates a Redis connection with optional SSH tunneling. + * @param {Object} config - Configuration for Redis. + * @param {number} [localPort=null] - Local port for SSH tunneling. + * @returns {Promise} The Redis client instance. + */ +const createRedisConnection = async (config, localPort = null) => { + const { use_ssh } = config.secretData; + const { host, port, password, database_number } = config.secretData; + + const connectionHost = use_ssh ? '127.0.0.1' : host; + const connectionPort = use_ssh ? localPort : port; + + const client = new Redis({ + host: connectionHost, + port: connectionPort, + db: database_number, + password, + connectTimeout: CONNECTION_TIMEOUT, + }); + + console.log(`Connecting to Redis at ${connectionHost}:${connectionPort}`); + + return new Promise((resolve, reject) => { + client.on('connect', () => { + console.log('Redis connection successful.'); + resolve(client); + }); + + client.on('error', (error) => { + console.error(`Redis connection failed: ${error.message}`); + reject(error); + }); + }); +}; + +/** + * Establishes an SSH tunnel for secure Redis connections. + * Provides port forwarding from a local port to the remote Redis server. + * @param {Object} sshConfig - SSH configuration. + * @param {string} redisHost - Redis host. + * @param {number} redisPort - Redis port. + * @returns {Promise} The SSH tunnel and local port. + */ +const createSSHTunnel = async (sshConfig, redisHost, redisPort) => { + try { + const { host, port, username, private_key_file, auth_method } = sshConfig; + const tunnelOptions = { autoClose: true }; + const forwardOptions = { dstAddr: redisHost, dstPort: redisPort }; + const sshOptions = { + host, + port, + username, + privateKey: auth_method === 'private_key' ? Buffer.from(private_key_file, 'utf-8') : undefined, + }; + + const [server, conn] = await createTunnel(tunnelOptions, null, sshOptions, forwardOptions); + const localPort = server.address().port; + console.log(`SSH tunnel established. Local port: ${localPort}`); + + // Attach error listeners to the SSH server and connection + server.on('error', (err) => { + throw new Error(`SSH Tunnel Server Error: ${err.message}`); + }); + conn.on('error', (err) => { + throw new Error(`SSH Tunnel Connection Error: ${err.message}`); + }); + + return { tunnel: server, localPort }; + } catch (err) { + console.error(`Failed to establish SSH tunnel: ${err.message}`); + throw err; + } +}; + +/** + * Retrieves an existing connection or creates a new one if not available. + * Ensures that the number of active connections does not exceed the allowed limit. + * @param {Object} config - Configuration for the connection. + * @returns {Promise} The Redis client instance. + */ +const getOrCreateConnection = async (config) => { + const configKey = JSON.stringify(config); + + // Return existing connection if available + if (connectionPool.has(configKey)) { + return connectionPool.get(configKey); + } + + // Check if connection pool has reached its limit + if (connectionPool.size >= MAX_CONNECTIONS) { + throw new Error('Connection limit reached.'); + } + + try { + // Create a new connection + const connection = await createRedisConnection(config); + + // Cache the newly created connection + connectionPool.set(configKey, connection); + return connection; + } catch (error) { + console.error('Error creating connection:', error.message); + throw error; + } +}; + +/** + * Express.js handler for incoming requests. + * Processes Redis commands based on provided configurations. + * @param {Object} req - Express request object. + * @param {Object} res - Express response object. + */ +export const handler = async (req, res) => { + try { + // Ensure the request method is POST + if (req.method !== 'POST') { + return res.status(400).json({ error: 'Invalid method, only POST allowed.' }); + } + + // Extract and validate request body + const body = req.body; + validateRequestBody(body); + + const { command, key, value, ttl, secretData } = body; + const client = await getOrCreateConnection({ secretData }); + + var result + if (command === "GET") { + result = await client.get(key); + } else if (command === "SET") { + result = ttl ? await client.set(key, value, "EX", ttl) : await client.set(key, value); + } + + console.log('Command executed successfully:', JSON.stringify(result)); + res.setHeader('Content-Type', 'application/json'); + res.status(200).json(result); + } catch (error) { + console.error('Error handling request:', error.message); + res.status(500).json({ error: error.message }); + } +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c5e42e4 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "boltic-redis-connector", + "version": "1.0.0", + "description": "The Redis connector enables seamless integration with any Redis database, allowing you to perform various operations efficiently.", + "main": "handler.js", + "scripts": { + "start": "node handler.js" + }, + "dependencies": { + "ioredis": "^5.4.1", + "tunnel-ssh": "^5.1.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "author": "Your Name", + "license": "MIT", + "type": "module" +} \ No newline at end of file