From dc437f50d5f9da3902746ccb23c14a9cc4cfbcc1 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Wed, 24 Sep 2025 15:17:28 -0600 Subject: [PATCH] servidor funcionando dashboard 80% --- docker-compose.yml | 4 +- freeradius/clients.conf | 8 ++- node-api/index.js | 113 +++++++++++++++++++++++++++++++++++++ node-api/package.json | 4 +- node-api/public/index.html | 52 ++++++++++++++++- 5 files changed, 175 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e8416c5..0b524db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,9 @@ services: - VLAN_ID=2 - MAX_UP=10000000 - MAX_DOWN=10000000 + - RADIUS_HOST=freeradius + - RADIUS_AUTH_PORT=1812 + - RADIUS_SECRET=${RADIUS_SHARED_SECRET:-testing123} networks: - radius_net @@ -35,4 +38,3 @@ services: networks: radius_net: driver: bridge - diff --git a/freeradius/clients.conf b/freeradius/clients.conf index be71df1..42688ea 100644 --- a/freeradius/clients.conf +++ b/freeradius/clients.conf @@ -1,5 +1,5 @@ client unifi { - ipaddr = 192.168.87.0/24 + ipaddr = 0.0.0.0/0 secret = tamosbien require_message_authenticator = no nastype = other @@ -10,3 +10,9 @@ client localhost { ipaddr = 127.0.0.1 secret = tamosbien } + +# Permitir contenedores en el bridge de Docker (laboratorio) +client docker_net_lab { + ipaddr = 172.16.0.0/12 + secret = tamosbien +} diff --git a/node-api/index.js b/node-api/index.js index bf4efd9..015f114 100644 --- a/node-api/index.js +++ b/node-api/index.js @@ -2,6 +2,8 @@ import express from 'express'; import morgan from 'morgan'; import path from 'path'; import { fileURLToPath } from 'url'; +import dgram from 'dgram'; +import radius from 'radius'; const app = express(); app.use(express.json()); @@ -16,6 +18,9 @@ const VLAN_ID = process.env.VLAN_ID || '2'; const MAX_UP = process.env.MAX_UP || '10000000'; // bits per second const MAX_DOWN = process.env.MAX_DOWN || '10000000'; // bits per second const MAX_REQUESTS = parseInt(process.env.MAX_REQUESTS || '200', 10); +const RADIUS_HOST = process.env.RADIUS_HOST || 'freeradius'; +const RADIUS_AUTH_PORT = parseInt(process.env.RADIUS_AUTH_PORT || '1812', 10); +const RADIUS_SECRET = process.env.RADIUS_SECRET || process.env.RADIUS_SHARED_SECRET || 'tamosbien'; // In-memory request store + SSE clients const requests = []; @@ -99,6 +104,49 @@ app.get('/api/requests', (req, res) => { res.json({ items: requests.slice(-MAX_REQUESTS) }); }); +// Clear recent requests +app.delete('/api/requests', (req, res) => { + requests.length = 0; + // Notify live clients to refresh if they want + const payload = `event: clear\n` + `data: {"ok":true}\n\n`; + for (const resSse of sseClients) { + try { resSse.write(payload); } catch {} + } + res.json({ ok: true }); +}); + +// Export CSV of recent requests +app.get('/api/requests.csv', (req, res) => { + const cols = [ + 'ts','type','user','nas','calling','called','decision','vlan','bw_down','bw_up' + ]; + const lines = [cols.join(',')]; + for (const ev of requests) { + const attrs = ev.attrs || {}; + const row = [ + ev.ts || '', + ev.type || '', + attrs['User-Name'] || attrs['User-Name*0'] || '', + attrs['NAS-IP-Address'] || attrs['NAS-Identifier'] || '', + attrs['Calling-Station-Id'] || '', + attrs['Called-Station-Id'] || '', + ev.decision || '', + ev.vlan || '', + (ev.bandwidth && ev.bandwidth.down) || '', + (ev.bandwidth && ev.bandwidth.up) || '' + ]; + const esc = (v) => String(v).includes(',') || String(v).includes('"') || String(v).includes('\n') + ? '"' + String(v).replace(/"/g, '""') + '"' + : String(v); + lines.push(row.map(esc).join(',')); + } + const csv = lines.join('\n'); + const ts = new Date().toISOString().replace(/[:T]/g, '-').split('.')[0]; + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename="radius-events-${ts}.csv"`); + res.send(csv); +}); + // SSE stream for live updates app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); @@ -117,6 +165,71 @@ app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); +// Self-test: send a RADIUS Access-Request to FreeRADIUS +async function sendRadiusSelfTest() { + return new Promise((resolve, reject) => { + try { + const packet = radius.encode({ + code: 'Access-Request', + secret: RADIUS_SECRET, + attributes: { + 'User-Name': 'selftest-node', + 'NAS-Identifier': 'node-dashboard', + 'Calling-Station-Id': '001122334455', + }, + }); + const client = dgram.createSocket('udp4'); + const started = Date.now(); + const timeout = setTimeout(() => { + client.close(); + reject(new Error('timeout')); + }, 4000); + client.on('message', (msg) => { + clearTimeout(timeout); + client.close(); + const res = radius.decode({ packet: msg, secret: RADIUS_SECRET }); + resolve({ + code: res.code, + rtt_ms: Date.now() - started, + }); + }); + client.send(packet, 0, packet.length, RADIUS_AUTH_PORT, RADIUS_HOST, (err) => { + if (err) { + clearTimeout(timeout); + client.close(); + reject(err); + } + }); + } catch (e) { + reject(e); + } + }); +} + +app.post('/test/radius', async (req, res) => { + try { + const result = await sendRadiusSelfTest(); + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'selftest', + attrs: { 'User-Name': 'selftest-node' }, + decision: result.code, + }); + res.json({ ok: true, result }); + } catch (err) { + pushRequest({ + id: Date.now() + ':' + Math.random().toString(16).slice(2), + ts: new Date().toISOString(), + type: 'selftest', + attrs: { 'User-Name': 'selftest-node' }, + decision: 'error', + error: String(err && err.message || err), + }); + res.status(500).json({ ok: false, error: String(err && err.message || err) }); + } +}); + const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Node RADIUS REST API listening on :${port}`); diff --git a/node-api/package.json b/node-api/package.json index ee0ac53..d4a9673 100644 --- a/node-api/package.json +++ b/node-api/package.json @@ -6,7 +6,7 @@ "type": "module", "dependencies": { "express": "^4.19.2", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "radius": "^1.1.0" } } - diff --git a/node-api/public/index.html b/node-api/public/index.html index 86f3482..b2e61ec 100644 --- a/node-api/public/index.html +++ b/node-api/public/index.html @@ -33,6 +33,11 @@
+ + Idle + + +
@@ -42,8 +47,14 @@ const list = document.getElementById('list'); const statusEl = document.getElementById('status'); const countEl = document.getElementById('count'); const btnRefresh = document.getElementById('refresh'); +const btnSelfTest = document.getElementById('selfTest'); +const selfTestStatus = document.getElementById('selfTestStatus'); const autoScroll = document.getElementById('autoScroll'); let total = 0; +const btnClear = document.getElementById('clear'); +const btnCopy = document.getElementById('copy'); +const btnExportCsv = document.getElementById('exportCsv'); +let history = []; function renderItem(ev) { const div = document.createElement('div'); @@ -69,12 +80,49 @@ async function loadHistory() { const r = await fetch('/api/requests'); const data = await r.json(); list.innerHTML = ''; + history = data.items || []; total = 0; - data.items.forEach(renderItem); + history.forEach(renderItem); } btnRefresh.addEventListener('click', loadHistory); +btnSelfTest.addEventListener('click', async () => { + selfTestStatus.textContent = 'Running…'; + try { + const r = await fetch('/test/radius', { method: 'POST' }); + const data = await r.json(); + if (data.ok) selfTestStatus.textContent = `OK (${data.result.code}, ${data.result.rtt_ms}ms)`; + else selfTestStatus.textContent = `Error (${data.error||r.status})`; + } catch (e) { + selfTestStatus.textContent = `Error (${e.message})`; + } +}); + +btnClear.addEventListener('click', async () => { + if (!confirm('¿Limpiar historial de eventos?')) return; + await fetch('/api/requests', { method: 'DELETE' }); + await loadHistory(); +}); + +btnCopy.addEventListener('click', async () => { + try { + await navigator.clipboard.writeText(JSON.stringify(history, null, 2)); + alert('Copiado al portapapeles'); + } catch (e) { + alert('No se pudo copiar: ' + e.message); + } +}); + +btnExportCsv.addEventListener('click', () => { + const a = document.createElement('a'); + a.href = '/api/requests.csv'; + a.download = ''; + document.body.appendChild(a); + a.click(); + a.remove(); +}); + // SSE live events function connectSSE() { const es = new EventSource('/events'); @@ -83,10 +131,10 @@ function connectSSE() { es.addEventListener('message', (e) => { try { const ev = JSON.parse(e.data); renderItem(ev); } catch {} }); + es.addEventListener('clear', () => { loadHistory(); }); } loadHistory().then(connectSSE); -