'use strict' /** * Proxy para a External API do motor NewWhats. * * Encaminha chamadas do satélite (Alemão) para o motor: * REST: GET|POST|... /api/nw/v1/* → {motorUrl}/api/ext/v1/* * WS: UPGRADE /api/nw/v1/stream → {motorUrl}/api/ext/v1/stream * * A integration_key é lida da tabela plugin_configs e injectada automaticamente * como header x-nw-key em todas as chamadas — nunca fica exposta ao browser. */ const http = require('http') const https = require('https') const net = require('net') const url = require('url') const { queryOne } = require('../../database/postgres') // ─── Helpers ────────────────────────────────────────────────────────────────── async function getPluginConfig() { const [keyRow, urlRow] = await Promise.all([ queryOne("SELECT value FROM plugin_configs WHERE plugin_id = 'newwhats' AND key = 'integration_key'"), queryOne("SELECT value FROM plugin_configs WHERE plugin_id = 'newwhats' AND key = 'newwhats_url'"), ]) return { integrationKey: keyRow?.value || null, motorUrl: urlRow?.value?.replace(/\/$/, '') || null, } } function notConfigured(res) { res.status(503).json({ error: 'Plugin NewWhats não configurado. Configure a integration_key e newwhats_url no admin.' }) } // ─── REST proxy ─────────────────────────────────────────────────────────────── /** * Express middleware que faz proxy de /api/nw/v1/* → motor /api/ext/v1/* * Preserva method, path, query string, body e headers (menos host). */ function createRestProxy() { return async function extApiProxy(req, res) { const { integrationKey, motorUrl } = await getPluginConfig() if (!integrationKey || !motorUrl) return notConfigured(res) const target = url.parse(motorUrl) const isHttps = target.protocol === 'https:' const lib = isHttps ? https : http // Remove /api/nw/v1 do prefix e substitui por /api/ext/v1 const suffix = req.path.replace(/^\/api\/nw\/v1/, '') const qs = req.originalUrl.split('?')[1] const fwdPath = `/api/ext/v1${suffix}${qs ? '?' + qs : ''}` const options = { hostname: target.hostname, port: target.port || (isHttps ? 443 : 80), path: fwdPath, method: req.method, headers: { ...req.headers, host: target.hostname, 'x-nw-key': integrationKey, // Remove headers que causam problemas no proxy connection: 'close', }, } const proxyReq = lib.request(options, (proxyRes) => { const status = proxyRes.statusCode || 502 // Se o motor retornar um redirect (3xx), ele pode incluir um header // Location apontando para sua própria porta (ex: :8008/api/auth/login). // O browser seguiria esse redirect diretamente para o motor — o que // causa ERR_CONNECTION_REFUSED porque a porta não está exposta. // Interceptamos aqui e devolvemos 401 para o browser tratar. if (status >= 300 && status < 400) { proxyRes.resume() // descarta o body return res.status(401).json({ error: 'Motor NewWhats requer autenticação. Verifique a integration_key no painel de Plugins.' }) } res.status(status) Object.entries(proxyRes.headers).forEach(([k, v]) => { // Nunca repassa Location ou Set-Cookie do motor — evita vazamento de URL interna if (k !== 'transfer-encoding' && k !== 'location' && k !== 'set-cookie') { res.setHeader(k, v) } }) proxyRes.pipe(res) }) proxyReq.on('error', (err) => { if (!res.headersSent) { res.status(502).json({ error: `Motor indisponível: ${err.message}` }) } }) // Forward body para POST/PUT/PATCH if (['POST', 'PUT', 'PATCH'].includes(req.method)) { const body = JSON.stringify(req.body) proxyReq.setHeader('content-length', Buffer.byteLength(body)) proxyReq.setHeader('content-type', 'application/json') proxyReq.write(body) } proxyReq.end() } } // ─── WS proxy (TCP tunnel) ──────────────────────────────────────────────────── /** * Adiciona handler de upgrade ao httpServer para fazer tunnel do WS. * * Fluxo: * Browser → UPGRADE /api/nw/v1/stream * Satélite → abre TCP para motor + injeta x-nw-key no upgrade * Tunnel bidirecional socket ↔ socket (zero overhead no runtime) */ function attachWsProxy(httpServer) { httpServer.on('upgrade', async (req, clientSocket, head) => { if (!req.url.startsWith('/api/nw/v1/stream')) return const { integrationKey, motorUrl } = await getPluginConfig() if (!integrationKey || !motorUrl) { clientSocket.write('HTTP/1.1 503 Service Unavailable\r\n\r\n') clientSocket.destroy() return } const target = url.parse(motorUrl) const port = parseInt(String(target.port || 80), 10) const host = target.hostname const proxySocket = net.connect(port, host, () => { // Reescreve o upgrade request com o path correto + x-nw-key const headers = Object.entries(req.headers) .filter(([k]) => k !== 'host' && k !== 'x-nw-key') .map(([k, v]) => `${k}: ${v}`) .join('\r\n') proxySocket.write( `${req.method} /api/ext/v1/stream HTTP/1.1\r\n` + `Host: ${host}\r\n` + `x-nw-key: ${integrationKey}\r\n` + `${headers}\r\n` + `\r\n` ) if (head && head.length) proxySocket.write(head) proxySocket.pipe(clientSocket) clientSocket.pipe(proxySocket) }) proxySocket.on('error', (err) => { console.error('[nw-proxy] WS tunnel error:', err.message) clientSocket.destroy() }) clientSocket.on('error', () => proxySocket.destroy()) clientSocket.on('close', () => proxySocket.destroy()) proxySocket.on('close', () => clientSocket.destroy()) }) } module.exports = { createRestProxy, attachWsProxy }