Files
radiusNucleo/node-api/index.js

124 lines
3.6 KiB
JavaScript

import express from 'express';
import morgan from 'morgan';
import path from 'path';
import { fileURLToPath } from 'url';
const app = express();
app.use(express.json());
app.use(morgan('dev'));
// Static files for dashboard
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(express.static(path.join(__dirname, 'public')));
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);
// In-memory request store + SSE clients
const requests = [];
const sseClients = new Set();
function pushRequest(rec) {
requests.push(rec);
while (requests.length > MAX_REQUESTS) requests.shift();
// Broadcast via SSE
const payload = `data: ${JSON.stringify(rec)}\n\n`;
for (const res of sseClients) {
try { res.write(payload); } catch { /* ignore */ }
}
}
// Helper: standard Accept with VLAN + bandwidth
function buildAcceptPayload(extra = {}) {
return {
control: {
'Auth-Type': 'Accept',
...extra.control,
},
reply: {
'Tunnel-Type': 'VLAN',
'Tunnel-Medium-Type': 'IEEE-802',
'Tunnel-Private-Group-Id': String(VLAN_ID),
'WISPr-Bandwidth-Max-Down': String(MAX_DOWN),
'WISPr-Bandwidth-Max-Up': String(MAX_UP),
...extra.reply,
},
};
}
// Normalize attributes from FreeRADIUS rlm_rest JSON
function normalizeAttributes(body = {}) {
// Newer rlm_rest may send attributes at top-level as { Attr: { type, value: [..] } }
// or under body.attributes / body.request as plain map.
const src = body.attributes || body.request || body;
const out = {};
for (const [k, v] of Object.entries(src || {})) {
if (v && typeof v === 'object' && Array.isArray(v.value)) out[k] = v.value[0];
else out[k] = v;
}
return out;
}
// Authorize endpoint: FreeRADIUS rlm_rest calls this in authorize {}
app.post('/authorize', (req, res) => {
console.log('--- RADIUS Authorize Request ---');
console.log(JSON.stringify(req.body, null, 2));
const attrs = normalizeAttributes(req.body);
const reply = buildAcceptPayload();
pushRequest({
id: Date.now() + ':' + Math.random().toString(16).slice(2),
ts: new Date().toISOString(),
type: 'authorize',
attrs,
decision: 'accept',
vlan: VLAN_ID,
bandwidth: { up: MAX_UP, down: MAX_DOWN },
});
return res.status(200).json(reply);
});
// Accounting endpoint (opcional)
app.post('/accounting', (req, res) => {
console.log('--- RADIUS Accounting ---');
console.log(JSON.stringify(req.body, null, 2));
pushRequest({
id: Date.now() + ':' + Math.random().toString(16).slice(2),
ts: new Date().toISOString(),
type: 'accounting',
attrs: normalizeAttributes(req.body),
});
return res.status(200).json({});
});
// API: recent requests
app.get('/api/requests', (req, res) => {
res.json({ items: requests.slice(-MAX_REQUESTS) });
});
// SSE stream for live updates
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders?.();
// send a hello event
res.write(`event: hello\n`);
res.write(`data: {"ok":true}\n\n`);
sseClients.add(res);
req.on('close', () => sseClients.delete(res));
});
// Root: serve dashboard
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Node RADIUS REST API listening on :${port}`);
});