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}`); });