170 lines
6.1 KiB
JavaScript
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 }
|