cgnat: add função

Signed-off-by: Rodrigo Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
Rodrigo Martins committed 2026-04-09 20:37:26 -03:00
1 parent f20c586f94
commit 6a5742ad7c
6 files changed
+10329 -1

No files matched your search

+207
View File
@@ -7,6 +7,7 @@ const { GenieACSManager } = require('../genieacs');
const config = require('../config/config');
const logger = require('../utils/logger');
const DateProcessor = require('../utils/dateProcessor');
const cgnatParser = require('../cgn_list/parser');
class BotHandler {
constructor(bot) {
@@ -288,6 +289,11 @@ class BotHandler {
await this.handleReiniciarOnu(chatId, target);
break;
case 'ver_cgnat':
console.log(`🌐 Executando handleVerCgnat com target: ${target}`);
await this.handleVerCgnat(chatId, userId, target);
break;
case 'conectar':
await this.handleConnect(chatId, target);
break;
@@ -1805,6 +1811,10 @@ Digite /help para ver todos os comandos disponíveis!
await this.handleDisconnectUser(chatId, text.trim());
this.userSessions.delete(userId);
return;
case 'cgnat_aguardando_datetime':
await this.handleCgnatDatetimeInput(chatId, userId, text.trim(), session);
return;
}
}
@@ -2142,6 +2152,11 @@ Digite /help para ver todos os comandos disponíveis!
await this._configurarRoteadorPorServico(chatId, userId, servico, location);
}
}
} else if (data.startsWith('buscar_cgnat_user_')) {
console.log(`🔍 Condição buscar_cgnat_user_ atendida`);
await this.bot.answerCallbackQuery(callbackQuery.id);
const login = data.replace('buscar_cgnat_user_', '');
await this.handleBuscarCliente(chatId, login);
} else if (data.startsWith('diag_cliente_')) {
console.log(`🔍 Condição diag_cliente_ atendida`);
await this.handleDiagClienteCallback(callbackQuery, data);
@@ -8858,6 +8873,198 @@ Digite /help para ver todos os comandos disponíveis!
}
}
/**
* Consulta CGNAT: dado IP público + porta opcional, informa o IP privado
* e opcionalmente busca no TopSapp quem estava usando aquele IP.
*/
async handleVerCgnat(chatId, userId, target) {
if (!target) {
await this.bot.sendMessage(chatId,
'🌐 *Consulta CGNAT*\n\n' +
'Informe o IP público e, opcionalmente, a porta:\n\n' +
'• `cgnat 131.0.193.0 1500` — busca por IP + porta\n' +
'• `cgnat 131.0.193.0` — lista todos os IPs privados do IP público',
{ parse_mode: 'Markdown' }
);
return;
}
// Parsear target: "IP" ou "IP PORTA"
const parts = target.trim().split(/\s+/);
const publicIp = parts[0];
const porta = parts[1] ? parseInt(parts[1], 10) : null;
// Validar IP
if (!/^\d+\.\d+\.\d+\.\d+$/.test(publicIp)) {
await this.bot.sendMessage(chatId, `❌ IP inválido: \`${publicIp}\``, { parse_mode: 'Markdown' });
return;
}
if (!cgnatParser.hasPublicIp(publicIp)) {
await this.bot.sendMessage(chatId, `❌ IP \`${publicIp}\` não encontrado na tabela CGNAT.`, { parse_mode: 'Markdown' });
return;
}
if (porta !== null) {
// Busca por IP + porta
const entry = cgnatParser.lookupByPort(publicIp, porta);
if (!entry) {
await this.bot.sendMessage(chatId,
`❌ Porta *${porta}* não encontrada nos ranges do IP \`${publicIp}\`.`,
{ parse_mode: 'Markdown' }
);
return;
}
await this.bot.sendMessage(chatId,
`🌐 *Resultado CGNAT*\n\n` +
`📡 IP Público: \`${publicIp}\`\n` +
`🔌 Porta: \`${porta}\`\n` +
`🖥️ IP Privado: \`${entry.privateIP}\`\n` +
`📊 Range de portas: \`${entry.portStart} – ${entry.portEnd}\`\n\n` +
`🔍 Deseja consultar quem usava este IP no TopSapp?\n` +
`Digite a data e hora do acesso no formato:\n` +
`\`DD/MM/AAAA HH:MM\`\n\n` +
`Ou envie *cancelar* para encerrar.`,
{ parse_mode: 'Markdown' }
);
this.userSessions.set(userId, {
type: 'cgnat_aguardando_datetime',
privateIP: entry.privateIP,
publicIP: publicIp,
porta
});
} else {
// Listar todos os IPs privados
const entries = cgnatParser.lookupAll(publicIp);
if (!entries || entries.length === 0) {
await this.bot.sendMessage(chatId, `❌ Nenhum IP privado encontrado para \`${publicIp}\`.`, { parse_mode: 'Markdown' });
return;
}
let msg = `🌐 *IPs privados do IP público \`${publicIp}\`*\n\n`;
for (const e of entries) {
msg += `• \`${e.privateIP}\` — portas \`${e.portStart}\` a \`${e.portEnd}\`\n`;
}
msg += `\n💡 Para identificar o cliente, informe também a porta:\n\`cgnat ${publicIp} PORTA\``;
await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' });
}
}
/**
* Recebe a data/hora e consulta o TopSapp por acessos do IP privado
*/
async handleCgnatDatetimeInput(chatId, userId, text, session) {
if (/^cancelar$/i.test(text)) {
this.userSessions.delete(userId);
await this.bot.sendMessage(chatId, '❌ Consulta cancelada.');
return;
}
// Aceitar formatos: "DD/MM/AAAA HH:MM" ou "DD/MM/AAAA"
const dtMatch = text.match(/^(\d{2}\/\d{2}\/\d{4})(?:\s+(\d{2}:\d{2}))?$/);
if (!dtMatch) {
await this.bot.sendMessage(chatId,
'❌ Formato inválido. Use `DD/MM/AAAA HH:MM` ou `DD/MM/AAAA`.\n' +
'Ou envie *cancelar* para encerrar.',
{ parse_mode: 'Markdown' }
);
return;
}
this.userSessions.delete(userId);
const dataStr = dtMatch[1]; // DD/MM/AAAA
const horaStr = dtMatch[2]; // HH:MM ou undefined
await this.bot.sendMessage(chatId,
`🔍 Consultando acessos de \`${session.privateIP}\` em ${dataStr}${horaStr ? ' ' + horaStr : ''}...`,
{ parse_mode: 'Markdown' }
);
const result = await this.topsapp.obterAcessosPorEstacao(
session.privateIP,
dataStr,
dataStr
);
if (!result.success) {
await this.bot.sendMessage(chatId,
`❌ Erro ao consultar TopSapp: ${result.error}`,
{ parse_mode: 'Markdown' }
);
return;
}
const acessos = result.data?.dados || [];
if (acessos.length === 0) {
await this.bot.sendMessage(chatId,
`ℹ️ Nenhum acesso encontrado para o IP \`${session.privateIP}\` em ${dataStr}.`,
{ parse_mode: 'Markdown' }
);
return;
}
// Filtrar por hora se informada
let filtrados = acessos;
if (horaStr) {
const [hh, mm] = horaStr.split(':').map(Number);
const refMs = hh * 3600000 + mm * 60000;
const janela = 3600000; // ±1 hora
filtrados = acessos.filter(a => {
const inicioMatch = a.inicio ? a.inicio.match(/(\d{2}):(\d{2})/) : null;
if (!inicioMatch) return true;
const aMs = parseInt(inicioMatch[1]) * 3600000 + parseInt(inicioMatch[2]) * 60000;
return Math.abs(aMs - refMs) <= janela;
});
}
if (filtrados.length === 0) {
await this.bot.sendMessage(chatId,
`ℹ️ Nenhum acesso encontrado para \`${session.privateIP}\` próximo às ${horaStr} em ${dataStr}.`,
{ parse_mode: 'Markdown' }
);
return;
}
let msg = `🌐 *Acessos CGNAT — \`${session.privateIP}\`*\n`;
msg += `📅 Data: ${dataStr}${horaStr ? ' ± 1h de ' + horaStr : ''}\n\n`;
for (const a of filtrados.slice(0, 10)) {
const inicio = a.inicio ? a.inicio.replace(/\.\d+.*$/, '').replace('T', ' ') : '—';
const termino = a.termino ? a.termino.replace(/\.\d+.*$/, '').replace('T', ' ') : 'ativo';
msg += `👤 *${a.usuario || '—'}*\n`;
msg += ` 🕐 ${inicio} → ${termino}\n`;
if (a.ip) msg += ` 🖥️ IP: \`${a.ip}\`\n`;
if (a.estacao) msg += ` 📡 Estação: ${a.estacao}\n`;
msg += '\n';
}
if (filtrados.length > 10) {
msg += `_... e mais ${filtrados.length - 10} registro(s)._\n`;
}
await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' });
// Se encontrou usuário(s), oferecer busca de nome do cliente
const logins = [...new Set(filtrados.slice(0, 10).map(a => a.usuario).filter(Boolean))];
if (logins.length === 1) {
const keyboard = {
inline_keyboard: [[
{ text: `🔍 Ver dados de ${logins[0]}`, callback_data: `buscar_cgnat_user_${logins[0]}` },
{ text: '❌ Fechar', callback_data: 'cancel_action' }
]]
};
await this.bot.sendMessage(chatId, '💡 Deseja ver os dados cadastrais do cliente?', {
reply_markup: keyboard
});
}
}
/**
* Reiniciar ONU Datacom — busca em todas as OLTs
*/
File diff suppressed because it is too large. Load diff
+80
View File
@@ -0,0 +1,80 @@
const fs = require('fs');
const path = require('path');
class CgnatParser {
constructor() {
this._data = null;
}
/**
* Carrega e parseia o nat_list.txt (lazy load).
* Retorna Map<publicIP, Array<{privateIP, portStart, portEnd}>>
*/
load() {
if (this._data) return this._data;
const filePath = path.join(__dirname, 'nat_list.txt');
const lines = fs.readFileSync(filePath, 'utf8').split('\n');
const data = new Map();
let currentPublicIp = null;
for (const line of lines) {
const trimmed = line.trim();
const natMatch = trimmed.match(/^NAT Address:\s+(\S+)/);
if (natMatch) {
currentPublicIp = natMatch[1];
if (!data.has(currentPublicIp)) {
data.set(currentPublicIp, []);
}
continue;
}
if (currentPublicIp) {
const entryMatch = trimmed.match(/^(\d+\.\d+\.\d+\.\d+)\s+(\d+)\s+to\s+(\d+)/);
if (entryMatch) {
data.get(currentPublicIp).push({
privateIP: entryMatch[1],
portStart: parseInt(entryMatch[2], 10),
portEnd: parseInt(entryMatch[3], 10)
});
}
}
}
this._data = data;
console.log(`📋 CgnatParser: ${data.size} IPs públicos carregados`);
return data;
}
/**
* Dado um IP público e porta, retorna a entrada { privateIP, portStart, portEnd } ou null.
*/
lookupByPort(publicIp, port) {
const data = this.load();
const entries = data.get(publicIp);
if (!entries) return null;
const portNum = parseInt(port, 10);
return entries.find(e => portNum >= e.portStart && portNum <= e.portEnd) || null;
}
/**
* Retorna todos os IPs privados mapeados a um IP público, ou null se não encontrado.
*/
lookupAll(publicIp) {
const data = this.load();
return data.get(publicIp) || null;
}
/**
* Verifica se um IP público existe no mapa CGNAT.
*/
hasPublicIp(publicIp) {
const data = this.load();
return data.has(publicIp);
}
}
module.exports = new CgnatParser();
+20 -1
View File
@@ -38,7 +38,8 @@ class NLPProcessor {
'ver_roteador': ['ver roteador', 'info roteador', 'status roteador', 'roteador', 'cpe', 'ver cpe', 'info cpe', 'dados roteador', 'informações roteador', 'informacoes roteador', 'genieacs', 'ver device', 'info device', 'dispositivo cpe'],
'mudar_ipoe': ['mudar para ipoe', 'mudar ipoe', 'ipoe', 'converter ipoe', 'ativar ipoe', 'habilitar ipoe', 'migrar ipoe', 'migrar para ipoe', 'converter para ipoe', 'passar para ipoe', 'trocar para ipoe', 'mudar pra ipoe'],
'configurar_roteador': ['configurar roteador', 'mudar roteador', 'alterar roteador', 'editar roteador', 'config roteador', 'configurar wifi', 'mudar wifi', 'alterar wifi', 'mudar ssid', 'alterar ssid', 'configurar cpe', 'mudar cpe', 'alterar cpe', 'configurar dns', 'mudar dns', 'configurar dhcp', 'mudar dhcp', 'alterar dhcp', 'mudar configuracoes roteador', 'mudar configurações roteador'],
'reiniciar_onu': ['reiniciar onu', 'reiniciar onu cliente', 'resetar onu', 'resetar onu cliente', 'reboot onu', 'restart onu', 'reiniciar ont', 'resetar ont', 'reboot ont', 'restart ont', 'reiniciar onu do cliente', 'reset onu cliente']
'reiniciar_onu': ['reiniciar onu', 'reiniciar onu cliente', 'resetar onu', 'resetar onu cliente', 'reboot onu', 'restart onu', 'reiniciar ont', 'resetar ont', 'reboot ont', 'restart ont', 'reiniciar onu do cliente', 'reset onu cliente'],
'ver_cgnat': ['cgnat', 'ver cgnat', 'consultar cgnat', 'cgn', 'nat publico', 'nat público', 'ip publico', 'ip público', 'ver nat', 'consultar nat', 'quem usa ip', 'ip privado cgnat', 'traducao nat', 'tradução nat']
};
// Padrões de comandos
@@ -99,6 +100,9 @@ class NLPProcessor {
// Padrões para consultar usuário específico
/^(consultar|ver|status|info|checar|verificar|pesquisar|localizar|buscar)\s+(usuario|user)\s+(.+)$/i,
/^(usuario|user)\s+(.+)$/i,
// Padrões para CGNAT
/^(cgnat|nat)\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(?:porta\s+)?(\d+))?$/i,
/^(ver|consultar)\s+(?:cgnat|nat)\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(?:porta\s+)?(\d+))?$/i,
// Padrões para diagnosticar cliente
/^(diagnosticar|diagnostico|diagnóstico|diagnose|analisar|inspecionar)\s+(cliente\s+)?(.+)$/i,
// Padrões para listar usuários
@@ -321,6 +325,21 @@ class NLPProcessor {
};
}
// Detectar consulta CGNAT
if (/^cgnat$|^nat$|^ver$|^consultar$/i.test(action) && /\d+\.\d+\.\d+\.\d+/.test(fullMatch)) {
// Extrair IP e porta dos grupos capturados
const ipPortMatch = fullMatch.match(/(\d+\.\d+\.\d+\.\d+)(?:\s+(?:porta\s+)?(\d+))?/i);
const ip = ipPortMatch ? ipPortMatch[1] : null;
const porta = ipPortMatch ? (ipPortMatch[2] || null) : null;
const target = porta ? `${ip} ${porta}` : ip;
return {
action: 'ver_cgnat',
target,
confidence: 0.98,
type: 'cgnat_pattern'
};
}
// Detectar diagnóstico de cliente
if (/^diagnosticar$|^diagnostico$|^diagnóstico$|^diagnose$|^analisar$|^inspecionar$/i.test(action)) {
// match[2] pode ser "cliente " ou já o nome, match[3] é o nome quando match[2]="cliente "
+7
View File
@@ -1006,6 +1006,13 @@ class TopSappAPI {
return cliente.status || 1;
}
/**
* Obter histórico de acessos por estação/IP (ObterClientesServicosAcessos)
*/
async obterAcessos(params, instance, credentials) {
return this.makeRequest('ObterClientesServicosAcessos', params, instance, credentials);
}
/**
* Calcular similaridade entre duas strings (algoritmo de Jaro-Winkler simplificado)
*/
+21
View File
@@ -1329,6 +1329,27 @@ class TopSappManager {
}
}
/**
* Busca acessos por estação/IP privado CGNAT no TopSapp
* @param {string} estacao - IP privado (ex: 100.110.0.5) ou nome da estação
* @param {string} dataInicial - formato dd/mm/yyyy
* @param {string} dataFinal - formato dd/mm/yyyy
* @param {string} location
*/
async obterAcessosPorEstacao(estacao, dataInicial, dataFinal, location = '') {
if (!this.isReady()) {
return { success: false, error: 'TopSapp não está configurado' };
}
try {
const credentials = this.config.getCredentialsByLocation(location);
const instance = this.api.getInstanceByLocation(location);
const params = { estacao, dataInicial, dataFinal };
return await this.api.obterAcessos(params, instance, credentials);
} catch (error) {
return { success: false, error: error.message };
}
}
}
module.exports = TopSappManager;