const fs = require('fs'); const https = require('https'); const WebSocket = require('ws'); const axios = require('axios'); require('dotenv').config(); const WS_URL = process.env.WS_URL; const WS_TOKEN = process.env.WS_TOKEN; if (!WS_URL) { console.error('❌ WS_URL is not set in .env'); process.exit(1); } const INITIAL_BACKOFF_MS = 2000; const MAX_BACKOFF_MS = 60000; const PROXY_TIMEOUT_MS = 30000; let ws = null; let reconnectAttempts = 0; let shuttingDown = false; /** * TLS options for wss:// and for HTTPS calls the agent proxies (SIW, MDM, …). * * Preferred: mount your corporate root + intermediate CA(s) as a PEM bundle * and point WS_TLS_CA_FILE at it. That keeps verification on. * * Escape hatch (trusted networks only): WS_TLS_REJECT_UNAUTHORIZED=false */ function readTlsOptions() { const tls = {}; const caFile = process.env.WS_TLS_CA_FILE; if (caFile) { try { tls.ca = fs.readFileSync(caFile, 'utf8'); console.log(`🔒 Using custom CA bundle: ${caFile}`); } catch (err) { console.error(`❌ Failed to read WS_TLS_CA_FILE (${caFile}): ${err.message}`); process.exit(1); } } if (process.env.WS_TLS_REJECT_UNAUTHORIZED !== undefined) { const reject = process.env.WS_TLS_REJECT_UNAUTHORIZED !== 'false' && process.env.WS_TLS_REJECT_UNAUTHORIZED !== '0'; tls.rejectUnauthorized = reject; if (!reject) { console.warn( '⚠️ WS_TLS_REJECT_UNAUTHORIZED=false — TLS certificate verification is DISABLED.' ); } } return Object.keys(tls).length ? tls : null; } const TLS_OPTIONS = readTlsOptions(); const HTTPS_AGENT = TLS_OPTIONS ? new https.Agent(TLS_OPTIONS) : undefined; /** * WebSocket client options: optional Bearer auth + optional TLS trust config. */ function buildClientOptions() { const options = {}; if (WS_TOKEN) { options.headers = { Authorization: `Bearer ${WS_TOKEN}` }; } if (TLS_OPTIONS) { Object.assign(options, TLS_OPTIONS); } return Object.keys(options).length ? options : undefined; } function connect() { console.log(`🔄 Connecting to ${WS_URL}...`); ws = new WebSocket(WS_URL, buildClientOptions()); ws.on('open', () => { console.log('✅ Remote Agent connected to StoreHealthAnalyzer'); reconnectAttempts = 0; }); ws.on('message', async data => { let request; try { request = JSON.parse(data); if (request.action !== 'proxyRequest') return; console.log(`🔄 Proxying ${request.method || 'GET'} ${request.url}`); const response = await axios({ method: request.method || 'GET', url: request.url, headers: request.headers || {}, auth: request.auth || undefined, data: request.body || undefined, timeout: PROXY_TIMEOUT_MS, httpsAgent: HTTPS_AGENT, }); ws.send( JSON.stringify({ requestId: request.requestId, status: response.status, data: response.data, headers: response.headers, }) ); } catch (err) { console.error('Proxy error:', err.message); ws.send( JSON.stringify({ requestId: request ? request.requestId : null, error: err.message, status: err.response?.status || 500, data: err.response?.data || null, }) ); } }); ws.on('close', code => { console.log(`❌ Disconnected (code: ${code}).`); if (!shuttingDown) scheduleReconnect(); }); ws.on('error', err => { console.error('WebSocket error:', err.message); }); } function scheduleReconnect() { reconnectAttempts++; const backoff = Math.min( INITIAL_BACKOFF_MS * Math.pow(1.5, reconnectAttempts - 1), MAX_BACKOFF_MS ); console.log( `⏳ Reconnecting in ${Math.round(backoff / 1000)}s... (attempt ${reconnectAttempts})` ); setTimeout(connect, backoff); } connect(); function shutdownRemote(signal) { console.log(`🛑 ${signal} received. Shutting down remote agent...`); shuttingDown = true; if (ws) { try { ws.close(); } catch (_e) { // ignore close errors during shutdown } } process.exit(0); } process.on('SIGINT', () => shutdownRemote('SIGINT')); process.on('SIGTERM', () => shutdownRemote('SIGTERM'));