Files
mercado.clube67.com/backend/plugins/newwhats/proxy.js
T

170 lines
6.1 KiB
JavaScript

'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 }